<commit_before>"use strict";

const Promise = require('bluebird');
const request = Promise.promisifyAll(require("request"));
const cheerio = require('cheerio');
const Food = require("../models/food");
const Menu = require("../models/menu");
const config = require('../config');
const timeHelper = require('../helpers/timeHelper');

var MensaUrl = config.scraper.mensaUrl;
var UniwirtUrl = config.scraper.uniwirtUrl;
var MittagstischUrl = config.scraper.mittagstischUrl;
var PizzeriaUrl = config.scraper.unipizzeriaUrl;

function parseWeek(input, parseFunction) {
    var menus = [];
    for (let day = 0; day < 7; day++) {
        menus[day] = parseFunction(input, day);
    }

    return menus;
}

function getUniwirtWeekPlan() {
    return request.getAsync(UniwirtUrl)
        .then(res => res.body)
        .then(body => parseWeek(body, parseUniwirt));
}

function getUniwirtPlan(day) {
    return request.getAsync(UniwirtUrl)
        .then(res => res.body)
        .then(body => parseUniwirt(body, day));
}

function parseUniwirt(html, day) {
    var result = new Menu();

    var $ = cheerio.load(html);
    var dayInWeek;
    if (day === null || day === undefined) {
        dayInWeek = ((new Date()).getDay() + 6) % 7;
    } else {
        dayInWeek = day;
    }

    var $currentDayRows = $("#StandardWrapper").find(".col600 > .col360.noMargin").eq(dayInWeek).find('tr');

    $currentDayRows.each((index, item) => {
        var $tds = $(item).children();
        var name = $tds.eq(0).text();
        name = sanitizeName(name);
        var price = $tds.eq(2).text();

        if (contains(name, true, ["feiertag", "ruhetag", "wir machen pause"])) {
            result.closed = true;
        } else if (!price) {
            result.starters.push(new Food(name));
        } else {
            price = +price.substring(2).replace(',', '.');
            result.mains.push(new Food(name, price));
        }
    });

    return setErrorOnEmpty(result);
}

function getMensaWeekPlan() {
    return request.getAsync(MensaUrl)
        .then(res => res.body)
        .then(body => parseWeek(body, parseMensa));
}

function getMensaPlan(day) {
    return request.getAsync(MensaUrl)
        .then(res => res.body)
        .then(body => parseMensa(body, day));
}

function parseMensa(html, day) {
    var result = new Menu();
    var $ = cheerio.load(html);
    var dayInWeek;
    if (day === null || day === undefined) {
        dayInWeek = ((new Date()).getDay() + 6) % 7;
    } else {
        dayInWeek = day;
    }

    if (dayInWeek > 4) {
        result.closed = true;
        return result;
    }

    var $classic1 = $('.day-content #category133');
    var $classic2 = $('.day-content #category134');
    var $dailySpecial = $('.day-content #category247');

    var currentDay1 = $classic1.find('.category-content').eq(dayInWeek).find('p');
    var currentDay2 = $classic2.find('.category-content').eq(dayInWeek).find('p');
    var currentDaySpecial = $dailySpecial.find('.category-content').eq(dayInWeek).find('p');

    if (contains(currentDay1.text(), true, ["feiertag", "ruhetag"]) ||
        contains(currentDay2.text(), true, ["feiertag", "ruhetag"]) ||
        contains(currentDaySpecial.text(), true, ["feiertag", "ruhetag"])) {
        result.closed = true;
        return result;
    }

    currentDay1 = currentDay1.map((index, item) => $(item).text()).filter(isNotBlank).toArray();
    currentDay2 = currentDay2.map((index, item) => $(item).text()).filter(isNotBlank).toArray();
    currentDaySpecial = currentDaySpecial.map((index, item) => $(item).text()).filter(isNotBlank).toArray();

    result.mains = [];
    if (currentDay1.length) {
        result.mains.push(createFoodFromMenuSection($classic1, currentDay1, dayInWeek));
    }
    if (currentDay2.length) {
        result.mains.push(createFoodFromMenuSection($classic2, currentDay2, dayInWeek));
    }
    if (currentDaySpecial.length) {
        result.mains.push(createFoodFromMenuSection($dailySpecial, currentDaySpecial, dayInWeek));
    }
    return setErrorOnEmpty(result);
}

function getUniPizzeriaWeekPlan() {
    return request.getAsync(PizzeriaUrl)
        .then(res => res.body)
        .then(body => parseWeek(parseUniPizzeria(body), getUniPizzeriaDayPlan));
}

function getUniPizzeriaPlan(day) {
    return request.getAsync(PizzeriaUrl)
        .then(res => res.body)
        .then(body => {
            var weekMenu = parseUniPizzeria(body);
            return getUniPizzeriaDayPlan(weekMenu, day);
        });
}

function getUniPizzeriaDayPlan(weekMenu, day) {
    var dayInWeek;
    if (day === null || day === undefined) {
        dayInWeek = ((new Date()).getDay() + 6) % 7;
    } else {
        dayInWeek = day;
    }

    var menu = new Menu();

    if (dayInWeek > 4) {
        let info = new Food("Kein Mittagsmenü.", null, true);
        menu.mains.push(info);
    } else if (dayInWeek < weekMenu.mains.length) {
        let combinedFood = weekMenu.mains[dayInWeek];

        //Handle holidays (no menu)
        if (contains(combinedFood.name, true, ["feiertag"])) {
            let info = new Food("Kein Mittagsmenü.", null, true);
            menu.mains.push(info);
        } else {

            let splitted = combinedFood.name.split("
");
            let starterExists = splitted.length > 1;

            if (starterExists) {
                //There is a starter
                let name = sanitizeName(splitted[0]);
                let starter = new Food(name);
                menu.starters.push(starter);
            }

            let i = starterExists ? 1 : 0;
            for (; i < splitted.length; i++) {
                let name = sanitizeName(splitted[i]);
                let main = new Food(name, combinedFood.price);
                menu.mains.push(main);
            }
        }
    }

    return setErrorOnEmpty(menu);
}

function parseUniPizzeria(html) {

    var result = new Menu();

    var $ = cheerio.load(html);
    var _days = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'je Mittagsteller € 7,80'];
    var _uniPizzeriaPrice = 7.80;

    var $menuContent = $('[itemprop="articleBody"]');

    var currentFood = null;
    $menuContent.find('p').each((index, item) => {

        var content = $(item).text().trim();

        // When the content of
 is a weekday, start new food
        if (_checkWeekday(content)) {
            if (currentFood != null) {
                result.mains.push(currentFood);
            }
            currentFood = new Food("", _uniPizzeriaPrice);
        }
        if (currentFood != null) {
            currentFood.name += _normalizeFood(content);
        }

    });

    if (currentFood != null && currentFood.name.length > 0) {
        result.mains.push(currentFood);
    }
    return setErrorOnEmpty(result);

    function _replaceWeekday(str) {
        for (var index in _days) {
            str = str.replace(_days[index], "");
        }
        return str;
    }

    function _checkWeekday(str) {
        var weekday = false;
        for (var index in _days) {
            if (str.indexOf(_days[index]) >= 0) {
                weekday = true;
            }
        }
        return weekday;
    }

    function _normalizeFood(str) {

        str = _replaceWeekday(str);
        str = str.replace(/\*+/, '
');

        return str;
    }

}

function createFoodFromMenuSection(section, menu, dayInWeek) {
    var price = null;
    var priceArray = section.eq(dayInWeek).find('.category-price').text().match(/€ (\S*)/);
    if (priceArray && priceArray.length) {
        price = priceArray[1];
        price = +price.replace(',', '.');
    }

    //isInfo <=> price could not get parsed (or is empty --> 0) and there is only one line of text
    var isInfo = (price == null || isNaN(price) || price === 0) && menu.length === 1;

    let name = sanitizeName(menu);
    return new Food(name, price, isInfo);
}

function getMittagstischWeekPlan() {
    return request.getAsync(MittagstischUrl)
        .then(res => res.body)
        .then(body => parseWeek(body, parseMittagstisch));
}

function getMittagstischPlan(day) {
    return request.getAsync(MittagstischUrl)
        .then(res => res.body)
        .then(body => parseMittagstisch(body, day));
}

function parseMittagstisch(body, day) {
    var foodMenu = new Menu();

    var dayInWeek;
    if (day === null || day === undefined) {
        dayInWeek = ((new Date()).getDay() + 6) % 7;
    } else {
        dayInWeek = day;
    }

    if (dayInWeek > 4) {
        foodMenu.closed = true;
        return foodMenu;
    }

    var $ = cheerio.load(body);

    var dayStates = $(".resp-tabs-list").children();
    var currentDayClosed = false;
    var closedDaysMap = {};
    var closedCnt = 0;

    //Count number of closed days before each day (so we can adjust indices by this amount
    dayStates.each((index, item) => {
        closedDaysMap["" + index] = closedCnt;

        if (item.attribs.class.indexOf("closed") > -1) {
            closedCnt++;

            if (index === dayInWeek) {
                currentDayClosed = true;
            }
        }
    });

    if (currentDayClosed) {
        foodMenu.closed = true;
        return foodMenu;
    }

    var closedDays = closedDaysMap["" + dayInWeek];

    var plans = $(".resp-tabs-container .daydata");

    //Wochentag nicht verfügbar
    if (plans.length <= dayInWeek * 2 - closedDays) {
        return foodMenu;
    }

    //Menü
    var menu = plans.eq(dayInWeek * 2 - closedDays);
    var menuRows = menu.find("tr");
    var first = menuRows.first();

    var soupRows = first.nextUntil('.free', 'tr');
    if (!first.hasClass("free")) {
        soupRows.unshift(first);
    }

    soupRows.splice(0, 1);
    soupRows.each((index, item) => {
        var name = $(item).children().first().text();
        name = sanitizeName(name);
        foodMenu.starters.push(new Food(name));
    });

    //Hauptspeisen
    var mainsRows = soupRows.last().next().nextUntil('.free', 'tr');
    mainsRows.splice(0, 1);

    mainsRows.each((index, item) => {
        var name = $(item).children().first().text();
        name = sanitizeName(name);
        foodMenu.mains.push(new Food(name, 7.4));
    });

    //á la carte
    var aLaCarte = plans.eq(dayInWeek * 2 + 1 - closedDays);
    var aLaCarteRows = aLaCarte.find("tr");
    first = aLaCarteRows.first();

    var aLaCartes = first.nextUntil('.free', 'tr');
    if (!first.hasClass("free") && aLaCartes.unshift) {
        aLaCartes.unshift(first);
    }

    aLaCartes.splice(0, 1);
    aLaCartes.each((index, item) => {
        var entries = $(item).children();
        var name = entries.first().text();
        name = sanitizeName(name);
        var price = +entries.eq(1).text().replace(",", ".");
        foodMenu.alacarte.push(new Food(name, price));
    });

    return setErrorOnEmpty(foodMenu);
}

function isNotBlank(index, element) {
    return element.length !== 0 && element.trim();
}

function contains(str, ignoreCase, searches) {
    if (!str)
        return false;
    if (ignoreCase) {
        str = str.toLowerCase();
    }

    for (var i = 0; i < searches.length; i++) {
        var search = ignoreCase ? searches[i].toLowerCase() : searches[i];
        if (str.includes(search)) {
            return true;
        }
    }
    return false;
}

function setErrorOnEmpty(menu) {
    if (!(menu.starters.length || menu.mains.length || menu.alacarte.length)) {
        menu.error = true;
    }
    return menu;
}

function sanitizeName(val) {
    if (typeof val === "string") {
	val = val.replace(/€\s[0-9](,|.)[0-9]+/, "");
        val = val.replace(/^[1-9].\s/, "");
        val = val.replace(/^[,\.\-\\\? ]+/, "");
        val = val.replace(/[,\.\-\\\? ]+$/, "");
        return val;
    } else if (typeof val === "object" && val.length > 0) {
        for (let i = 0; i < val.length; i++) {
            val[i] = sanitizeName(val[i]);
        }
        return val;
    } else {
        return val;
    }
}

module.exports = {
    getUniwirtPlan: getUniwirtPlan,
    getUniwirtWeekPlan: getUniwirtWeekPlan,
    getMittagstischPlan: getMittagstischPlan,
    getMittagstischWeekPlan: getMittagstischWeekPlan,
    getMensaPlan: getMensaPlan,
    getMensaWeekPlan: getMensaWeekPlan,
    getUniPizzeriaPlan: getUniPizzeriaPlan,
    getUniPizzeriaWeekPlan: getUniPizzeriaWeekPlan
};
<commit_msg>Added comments to regexes from  @behackl<commit_after>"use strict";

const Promise = require('bluebird');
const request = Promise.promisifyAll(require("request"));
const cheerio = require('cheerio');
const Food = require("../models/food");
const Menu = require("../models/menu");
const config = require('../config');
const timeHelper = require('../helpers/timeHelper');

var MensaUrl = config.scraper.mensaUrl;
var UniwirtUrl = config.scraper.uniwirtUrl;
var MittagstischUrl = config.scraper.mittagstischUrl;
var PizzeriaUrl = config.scraper.unipizzeriaUrl;

function parseWeek(input, parseFunction) {
    var menus = [];
    for (let day = 0; day < 7; day++) {
        menus[day] = parseFunction(input, day);
    }

    return menus;
}

function getUniwirtWeekPlan() {
    return request.getAsync(UniwirtUrl)
        .then(res => res.body)
        .then(body => parseWeek(body, parseUniwirt));
}

function getUniwirtPlan(day) {
    return request.getAsync(UniwirtUrl)
        .then(res => res.body)
        .then(body => parseUniwirt(body, day));
}

function parseUniwirt(html, day) {
    var result = new Menu();

    var $ = cheerio.load(html);
    var dayInWeek;
    if (day === null || day === undefined) {
        dayInWeek = ((new Date()).getDay() + 6) % 7;
    } else {
        dayInWeek = day;
    }

    var $currentDayRows = $("#StandardWrapper").find(".col600 > .col360.noMargin").eq(dayInWeek).find('tr');

    $currentDayRows.each((index, item) => {
        var $tds = $(item).children();
        var name = $tds.eq(0).text();
        name = sanitizeName(name);
        var price = $tds.eq(2).text();

        if (contains(name, true, ["feiertag", "ruhetag", "wir machen pause"])) {
            result.closed = true;
        } else if (!price) {
            result.starters.push(new Food(name));
        } else {
            price = +price.substring(2).replace(',', '.');
            result.mains.push(new Food(name, price));
        }
    });

    return setErrorOnEmpty(result);
}

function getMensaWeekPlan() {
    return request.getAsync(MensaUrl)
        .then(res => res.body)
        .then(body => parseWeek(body, parseMensa));
}

function getMensaPlan(day) {
    return request.getAsync(MensaUrl)
        .then(res => res.body)
        .then(body => parseMensa(body, day));
}

function parseMensa(html, day) {
    var result = new Menu();
    var $ = cheerio.load(html);
    var dayInWeek;
    if (day === null || day === undefined) {
        dayInWeek = ((new Date()).getDay() + 6) % 7;
    } else {
        dayInWeek = day;
    }

    if (dayInWeek > 4) {
        result.closed = true;
        return result;
    }

    var $classic1 = $('.day-content #category133');
    var $classic2 = $('.day-content #category134');
    var $dailySpecial = $('.day-content #category247');

    var currentDay1 = $classic1.find('.category-content').eq(dayInWeek).find('p');
    var currentDay2 = $classic2.find('.category-content').eq(dayInWeek).find('p');
    var currentDaySpecial = $dailySpecial.find('.category-content').eq(dayInWeek).find('p');

    if (contains(currentDay1.text(), true, ["feiertag", "ruhetag"]) ||
        contains(currentDay2.text(), true, ["feiertag", "ruhetag"]) ||
        contains(currentDaySpecial.text(), true, ["feiertag", "ruhetag"])) {
        result.closed = true;
        return result;
    }

    currentDay1 = currentDay1.map((index, item) => $(item).text()).filter(isNotBlank).toArray();
    currentDay2 = currentDay2.map((index, item) => $(item).text()).filter(isNotBlank).toArray();
    currentDaySpecial = currentDaySpecial.map((index, item) => $(item).text()).filter(isNotBlank).toArray();

    result.mains = [];
    if (currentDay1.length) {
        result.mains.push(createFoodFromMenuSection($classic1, currentDay1, dayInWeek));
    }
    if (currentDay2.length) {
        result.mains.push(createFoodFromMenuSection($classic2, currentDay2, dayInWeek));
    }
    if (currentDaySpecial.length) {
        result.mains.push(createFoodFromMenuSection($dailySpecial, currentDaySpecial, dayInWeek));
    }
    return setErrorOnEmpty(result);
}

function getUniPizzeriaWeekPlan() {
    return request.getAsync(PizzeriaUrl)
        .then(res => res.body)
        .then(body => parseWeek(parseUniPizzeria(body), getUniPizzeriaDayPlan));
}

function getUniPizzeriaPlan(day) {
    return request.getAsync(PizzeriaUrl)
        .then(res => res.body)
        .then(body => {
            var weekMenu = parseUniPizzeria(body);
            return getUniPizzeriaDayPlan(weekMenu, day);
        });
}

function getUniPizzeriaDayPlan(weekMenu, day) {
    var dayInWeek;
    if (day === null || day === undefined) {
        dayInWeek = ((new Date()).getDay() + 6) % 7;
    } else {
        dayInWeek = day;
    }

    var menu = new Menu();

    if (dayInWeek > 4) {
        let info = new Food("Kein Mittagsmenü.", null, true);
        menu.mains.push(info);
    } else if (dayInWeek < weekMenu.mains.length) {
        let combinedFood = weekMenu.mains[dayInWeek];

        //Handle holidays (no menu)
        if (contains(combinedFood.name, true, ["feiertag"])) {
            let info = new Food("Kein Mittagsmenü.", null, true);
            menu.mains.push(info);
        } else {

            let splitted = combinedFood.name.split("
");
            let starterExists = splitted.length > 1;

            if (starterExists) {
                //There is a starter
                let name = sanitizeName(splitted[0]);
                let starter = new Food(name);
                menu.starters.push(starter);
            }

            let i = starterExists ? 1 : 0;
            for (; i < splitted.length; i++) {
                let name = sanitizeName(splitted[i]);
                let main = new Food(name, combinedFood.price);
                menu.mains.push(main);
            }
        }
    }

    return setErrorOnEmpty(menu);
}

function parseUniPizzeria(html) {

    var result = new Menu();

    var $ = cheerio.load(html);
    var _days = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'je Mittagsteller € 7,80'];
    var _uniPizzeriaPrice = 7.80;

    var $menuContent = $('[itemprop="articleBody"]');

    var currentFood = null;
    $menuContent.find('p').each((index, item) => {

        var content = $(item).text().trim();

        // When the content of
 is a weekday, start new food
        if (_checkWeekday(content)) {
            if (currentFood != null) {
                result.mains.push(currentFood);
            }
            currentFood = new Food("", _uniPizzeriaPrice);
        }
        if (currentFood != null) {
            currentFood.name += _normalizeFood(content);
        }

    });

    if (currentFood != null && currentFood.name.length > 0) {
        result.mains.push(currentFood);
    }
    return setErrorOnEmpty(result);

    function _replaceWeekday(str) {
        for (var index in _days) {
            str = str.replace(_days[index], "");
        }
        return str;
    }

    function _checkWeekday(str) {
        var weekday = false;
        for (var index in _days) {
            if (str.indexOf(_days[index]) >= 0) {
                weekday = true;
            }
        }
        return weekday;
    }

    function _normalizeFood(str) {

        str = _replaceWeekday(str);
        str = str.replace(/\*+/, '
');

        return str;
    }

}

function createFoodFromMenuSection(section, menu, dayInWeek) {
    var price = null;
    var priceArray = section.eq(dayInWeek).find('.category-price').text().match(/€ (\S*)/);
    if (priceArray && priceArray.length) {
        price = priceArray[1];
        price = +price.replace(',', '.');
    }

    //isInfo <=> price could not get parsed (or is empty --> 0) and there is only one line of text
    var isInfo = (price == null || isNaN(price) || price === 0) && menu.length === 1;

    let name = sanitizeName(menu);
    return new Food(name, price, isInfo);
}

function getMittagstischWeekPlan() {
    return request.getAsync(MittagstischUrl)
        .then(res => res.body)
        .then(body => parseWeek(body, parseMittagstisch));
}

function getMittagstischPlan(day) {
    return request.getAsync(MittagstischUrl)
        .then(res => res.body)
        .then(body => parseMittagstisch(body, day));
}

function parseMittagstisch(body, day) {
    var foodMenu = new Menu();

    var dayInWeek;
    if (day === null || day === undefined) {
        dayInWeek = ((new Date()).getDay() + 6) % 7;
    } else {
        dayInWeek = day;
    }

    if (dayInWeek > 4) {
        foodMenu.closed = true;
        return foodMenu;
    }

    var $ = cheerio.load(body);

    var dayStates = $(".resp-tabs-list").children();
    var currentDayClosed = false;
    var closedDaysMap = {};
    var closedCnt = 0;

    //Count number of closed days before each day (so we can adjust indices by this amount
    dayStates.each((index, item) => {
        closedDaysMap["" + index] = closedCnt;

        if (item.attribs.class.indexOf("closed") > -1) {
            closedCnt++;

            if (index === dayInWeek) {
                currentDayClosed = true;
            }
        }
    });

    if (currentDayClosed) {
        foodMenu.closed = true;
        return foodMenu;
    }

    var closedDays = closedDaysMap["" + dayInWeek];

    var plans = $(".resp-tabs-container .daydata");

    //Wochentag nicht verfügbar
    if (plans.length <= dayInWeek * 2 - closedDays) {
        return foodMenu;
    }

    //Menü
    var menu = plans.eq(dayInWeek * 2 - closedDays);
    var menuRows = menu.find("tr");
    var first = menuRows.first();

    var soupRows = first.nextUntil('.free', 'tr');
    if (!first.hasClass("free")) {
        soupRows.unshift(first);
    }

    soupRows.splice(0, 1);
    soupRows.each((index, item) => {
        var name = $(item).children().first().text();
        name = sanitizeName(name);
        foodMenu.starters.push(new Food(name));
    });

    //Hauptspeisen
    var mainsRows = soupRows.last().next().nextUntil('.free', 'tr');
    mainsRows.splice(0, 1);

    mainsRows.each((index, item) => {
        var name = $(item).children().first().text();
        name = sanitizeName(name);
        foodMenu.mains.push(new Food(name, 7.4));
    });

    //á la carte
    var aLaCarte = plans.eq(dayInWeek * 2 + 1 - closedDays);
    var aLaCarteRows = aLaCarte.find("tr");
    first = aLaCarteRows.first();

    var aLaCartes = first.nextUntil('.free', 'tr');
    if (!first.hasClass("free") && aLaCartes.unshift) {
        aLaCartes.unshift(first);
    }

    aLaCartes.splice(0, 1);
    aLaCartes.each((index, item) => {
        var entries = $(item).children();
        var name = entries.first().text();
        name = sanitizeName(name);
        var price = +entries.eq(1).text().replace(",", ".");
        foodMenu.alacarte.push(new Food(name, price));
    });

    return setErrorOnEmpty(foodMenu);
}

function isNotBlank(index, element) {
    return element.length !== 0 && element.trim();
}

function contains(str, ignoreCase, searches) {
    if (!str)
        return false;
    if (ignoreCase) {
        str = str.toLowerCase();
    }

    for (var i = 0; i < searches.length; i++) {
        var search = ignoreCase ? searches[i].toLowerCase() : searches[i];
        if (str.includes(search)) {
            return true;
        }
    }
    return false;
}

function setErrorOnEmpty(menu) {
    if (!(menu.starters.length || menu.mains.length || menu.alacarte.length)) {
        menu.error = true;
    }
    return menu;
}

function sanitizeName(val) {
    if (typeof val === "string") {
	      val = val.replace(/€\s[0-9](,|.)[0-9]+/, ""); // Replace '€ 00.00'
        val = val.replace(/^[1-9].\s/, ""); // Replace '1. ', '2. '
        val = val.replace(/^[,\.\-\\\? ]+/, "");
        val = val.replace(/[,\.\-\\\? ]+$/, "");
        return val;
    } else if (typeof val === "object" && val.length > 0) {
        for (let i = 0; i < val.length; i++) {
            val[i] = sanitizeName(val[i]);
        }
        return val;
    } else {
        return val;
    }
}

module.exports = {
    getUniwirtPlan: getUniwirtPlan,
    getUniwirtWeekPlan: getUniwirtWeekPlan,
    getMittagstischPlan: getMittagstischPlan,
    getMittagstischWeekPlan: getMittagstischWeekPlan,
    getMensaPlan: getMensaPlan,
    getMensaWeekPlan: getMensaWeekPlan,
    getUniPizzeriaPlan: getUniPizzeriaPlan,
    getUniPizzeriaWeekPlan: getUniPizzeriaWeekPlan
};
<|endoftext|>


<commit_before>import path from 'path';

import * as config from '../../config.js';

const projectRoot = path.resolve(__dirname, '../..');

/**
 * The base directory (absolute path!) for resolving the entry option.
 * If output.pathinfo is set, the included pathinfo is shortened to this directory.
 */
const CONTEXT = projectRoot;

/**
 * The entry point for the bundle.
 * If you pass a string: The string is resolved to a module which is loaded upon startup.
 */
const ENTRY = './src/app/main';

/**
 * Options affecting the output of the compilation.
 * Output options tell Webpack how to write the compiled files to disk.
 */
const OUTPUT = {};

/**
 * Specifies the name of each output file on disk.
 * Cannot be a path, which is specified elsewhere.
 */
OUTPUT.filename = 'main.bundle.js';

/**
 * The output directory as absolute path.
 */
OUTPUT.path = path.resolve(projectRoot, 'dist/app');

/**
 * The publicPath specifies the public URL address of the output files when referenced in a browser.
 */
OUTPUT.publicPath = '/';

/**
 * Options affecting the normal modules
 */
const MODULE = {};

/**
 * Loaders that affect file transformation
 */
const typescriptLoader = {
  test: /\.tsx?$/,
  loader: 'ts-loader'
};

MODULE.loaders = [typescriptLoader];

/**
 * Webpack dev server
 */
const DEV_SERVER = {};

/**
 * Port for the webpack dev server to run
 */
DEV_SERVER.port = config.PORT;

/**
 * Serve static assets from build directory
 */
DEV_SERVER.contentBase = path.resolve(projectRoot, 'dist');

/**
 * Make webpack dev server less noisy
 */
DEV_SERVER.stats = {
  assets: false,
  colors: true,
  version: false,
  hash: false,
  timings: false,
  chunks: true,
  chunkModules: false
};

/**
 * Webpack config factory
 */
export function webpackConfig() {
  return {
    context: CONTEXT,
    entry: ENTRY,
    output: OUTPUT,
    module: MODULE
  };
}

/**
 * Webpack dev server watch config factory
 */
export function webpackWatchConfig() {
  return {
    context: CONTEXT,
    entry: ENTRY,
    output: OUTPUT,
    module: MODULE,
    devServer: DEV_SERVER
  };
}

/**
 * Get an array of scripts and stylesheets for normal builds, meant for production use
 */
export function webpackAssets() {
  const scripts = [`/app/${OUTPUT.filename}`];
  const stylesheets = [];
  return { scripts, stylesheets };
}

/**
 * Get an array of scripts and stylesheets for watch builds, meant for development use
 */
export function webpackWatchAssets() {
  const scripts = [
    `/${OUTPUT.filename}`
  ];
  const stylesheets = [];
  return { scripts, stylesheets };
}
<commit_msg>Fix array formatting<commit_after>import path from 'path';

import * as config from '../../config.js';

const projectRoot = path.resolve(__dirname, '../..');

/**
 * The base directory (absolute path!) for resolving the entry option.
 * If output.pathinfo is set, the included pathinfo is shortened to this directory.
 */
const CONTEXT = projectRoot;

/**
 * The entry point for the bundle.
 * If you pass a string: The string is resolved to a module which is loaded upon startup.
 */
const ENTRY = './src/app/main';

/**
 * Options affecting the output of the compilation.
 * Output options tell Webpack how to write the compiled files to disk.
 */
const OUTPUT = {};

/**
 * Specifies the name of each output file on disk.
 * Cannot be a path, which is specified elsewhere.
 */
OUTPUT.filename = 'main.bundle.js';

/**
 * The output directory as absolute path.
 */
OUTPUT.path = path.resolve(projectRoot, 'dist/app');

/**
 * The publicPath specifies the public URL address of the output files when referenced in a browser.
 */
OUTPUT.publicPath = '/';

/**
 * Options affecting the normal modules
 */
const MODULE = {};

/**
 * Loaders that affect file transformation
 */
const typescriptLoader = {
  test: /\.tsx?$/,
  loader: 'ts-loader'
};

MODULE.loaders = [typescriptLoader];

/**
 * Webpack dev server
 */
const DEV_SERVER = {};

/**
 * Port for the webpack dev server to run
 */
DEV_SERVER.port = config.PORT;

/**
 * Serve static assets from build directory
 */
DEV_SERVER.contentBase = path.resolve(projectRoot, 'dist');

/**
 * Make webpack dev server less noisy
 */
DEV_SERVER.stats = {
  assets: false,
  colors: true,
  version: false,
  hash: false,
  timings: false,
  chunks: true,
  chunkModules: false
};

/**
 * Webpack config factory
 */
export function webpackConfig() {
  return {
    context: CONTEXT,
    entry: ENTRY,
    output: OUTPUT,
    module: MODULE
  };
}

/**
 * Webpack dev server watch config factory
 */
export function webpackWatchConfig() {
  return {
    context: CONTEXT,
    entry: ENTRY,
    output: OUTPUT,
    module: MODULE,
    devServer: DEV_SERVER
  };
}

/**
 * Get an array of scripts and stylesheets for normal builds, meant for production use
 */
export function webpackAssets() {
  const scripts = [`/app/${OUTPUT.filename}`];
  const stylesheets = [];
  return { scripts, stylesheets };
}

/**
 * Get an array of scripts and stylesheets for watch builds, meant for development use
 */
export function webpackWatchAssets() {
  const scripts = [`/${OUTPUT.filename}`];
  const stylesheets = [];
  return { scripts, stylesheets };
}
<|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 {userCan} from 'src/common/util'
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, roundDecimal} from 'src/server/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) {
  const {playerIds, retrospectiveSurveyId} = project
  if (!playerIds || playerIds.length === 0) {
    throw new Error(`No players found on team for project ${project.id}`)
  }
  if (!retrospectiveSurveyId) {
    throw new Error(`Retrospective survey ID not set for project ${project.id}`)
  }

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

  const statsQuestions = await _getStatsQuestions(retroQuestions)
  const playerResponsesById = _getPlayerResponsesBySubjectId(retroResponses, retroQuestions)

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

  // compute updated Elo ratings and merge them in
  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}}
  })

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

  await Promise.all(playerStatsUpdates)
}

function _getPlayerResponsesBySubjectId(retroResponses, retroQuestions) {
  const retroQuestionsById = mapById(retroQuestions)
  const playerResponses = retroResponses.filter(response => {
    const responseQuestion = retroQuestionsById.get(response.questionId)
    const {subjectType} = responseQuestion || {}
    return subjectType === 'player' || subjectType === 'team'
  })

  return groupResponsesBySubject(playerResponses)
}

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())
}

async function _computeStatsClosure(project, retroResponses, statsQuestions, playerStatsConfigsById) {
  const projectTeamPlayers = await findPlayersByIds(project.playerIds)
  const teamPlayersById = mapById(projectTeamPlayers)
  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)
    if (!player) {
      console.error(new Error(`Survey responses found for a player ${playerId} who is not on project ${project.id}; player stats skipped`))
      return
    }

    const scores = _extractPlayerScores(statsQuestions, responses, playerId)

    const stats = {}
    stats.playerId = playerId // will be removed later
    stats.teamHours = teamHours
    stats.hours = teamPlayerHours.get(playerId) || 0
    stats.challenge = teamPlayerChallenges.get(playerId)
    stats.abc = aggregateBuildCycles(project.playerIds.length)
    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.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 _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>extract Elo-merging logic into its own function<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 {userCan} from 'src/common/util'
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, roundDecimal} from 'src/server/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) {
  const {playerIds, retrospectiveSurveyId} = project
  if (!playerIds || playerIds.length === 0) {
    throw new Error(`No players found on team for project ${project.id}`)
  }
  if (!retrospectiveSurveyId) {
    throw new Error(`Retrospective survey ID not set for project ${project.id}`)
  }

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

  const statsQuestions = await _getStatsQuestions(retroQuestions)
  const playerResponsesById = _getPlayerResponsesBySubjectId(retroResponses, retroQuestions)

  // compute all stats and initialize Elo rating
  const playerStatsConfigsById = await _getPlayersStatsConfig(project.playerIds)
  const computeStats = await _computeStatsClosure(project, 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 _getPlayerResponsesBySubjectId(retroResponses, retroQuestions) {
  const retroQuestionsById = mapById(retroQuestions)
  const playerResponses = retroResponses.filter(response => {
    const responseQuestion = retroQuestionsById.get(response.questionId)
    const {subjectType} = responseQuestion || {}
    return subjectType === 'player' || subjectType === 'team'
  })

  return groupResponsesBySubject(playerResponses)
}

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())
}

async function _computeStatsClosure(project, retroResponses, statsQuestions, playerStatsConfigsById) {
  const projectTeamPlayers = await findPlayersByIds(project.playerIds)
  const teamPlayersById = mapById(projectTeamPlayers)
  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)
    if (!player) {
      console.error(new Error(`Survey responses found for a player ${playerId} who is not on project ${project.id}; player stats skipped`))
      return
    }

    const scores = _extractPlayerScores(statsQuestions, responses, playerId)

    const stats = {}
    stats.playerId = playerId // will be removed later
    stats.teamHours = teamHours
    stats.hours = teamPlayerHours.get(playerId) || 0
    stats.challenge = teamPlayerChallenges.get(playerId)
    stats.abc = aggregateBuildCycles(project.playerIds.length)
    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.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
}
<|endoftext|>


<commit_before>const DrawCard = require('../../../drawcard.js');

class QuietAsAShadow extends DrawCard {
    setupCardAbilities() {
        this.action({
            title: 'Give character stealth',
            target: {
                activePromptTitle: 'Select a character',
                cardCondition: card => card.isUnique() && card.getType() === 'character' && card.getCost() <= 3
            },
            handler: context => {
                this.untilEndOfPhase(ability => ({
                    match: context.target,
                    effect: ability.effects.addKeyword('stealth')
                }));
                this.game.addMessage('{0} plays {1} to give {2} stealth until the end of the phase',
                    this.controller, this, context.target);
            }
        });
    }
}

QuietAsAShadow.code = '04099';

module.exports = QuietAsAShadow;
<commit_msg>Only allow Quiet as a Shadow to target in play cards (#1402)<commit_after>const DrawCard = require('../../../drawcard.js');

class QuietAsAShadow extends DrawCard {
    setupCardAbilities() {
        this.action({
            title: 'Give character stealth',
            target: {
                activePromptTitle: 'Select a character',
                cardCondition: card => card.isUnique() && card.getType() === 'character' && card.getCost() <= 3 && card.location === 'play area'
            },
            handler: context => {
                this.untilEndOfPhase(ability => ({
                    match: context.target,
                    effect: ability.effects.addKeyword('stealth')
                }));
                this.game.addMessage('{0} plays {1} to give {2} stealth until the end of the phase',
                    this.controller, this, context.target);
            }
        });
    }
}

QuietAsAShadow.code = '04099';

module.exports = QuietAsAShadow;
<|endoftext|>


<commit_before>(function () {

function Data (data) {
  this.data = data;
}

Data.prototype = {

  bound : function (min, max) {

    if (!_.isNumber(min) || !_.isNumber(max)) return this;

    var
      data    = this.data,
      length  = data.length,
      newData = [],
      i;

    for (i = 0; i < length; i++) {
      if (data[i][0] >= min) break;
    }

    if (i > 0) i--;

    for (i; i < length; i++) {
      newData.push(data[i]);
      if (data[i][0] > max) break;
    }

    this.data = newData;

    return this;
  },
  /**
   * Sample using min and max.
   */
  subsampleMinMax : function (resolution) {

    var
      data    = this.data,
      length  = data.length,
      _length = length - 1,
      newData = [],
      unit    = resolution / length,
      index   = Math.round(unit * 1),
      iMin    = 1,
      iMax    = 1,
      min, max, datum, i, j;

    if (length > resolution) {
      newData.push(data[0]);
      for (i = 1; i < _length; i++) {
        j = Math.round(unit * i);

        // New index
        if (j !== index) {
          if (iMax === iMin) {
            newData.push(data[iMin]);
          } else {
            newData.push(data[Math.min(iMin, iMax)], data[Math.max(iMin, iMax)]);
          }
          iMin = iMax = i;
          min = max = data[i][1];
          index = j;
        }

        // Old index
        else {
          datum = data[i][1];
          if (datum < min) {
            iMin = i;
            min = datum;
          }
          if (datum > max) {
            iMax = i;
            max = datum;
          }
        }
      }
      newData.push(data[length-1]);
      this.data = newData;
    }

    return this;
  },

  subsample : function (resolution) {

    var
      data    = this.data,
      length  = data.length,
      newData = [],
      unit    = length / resolution,
      i;

    if (length > resolution) {
      newData.push(data[0]);
      for (i = 1; i < resolution; i++) {
        if (i * unit >= length)
          break;
        newData.push(data[Math.round(i*unit)]);
      }
      newData.push(data[length-1]);
      this.data = newData;
    }

    return this;
  }
}

humblevis.Data = Data;

}());
<commit_msg>Fixed missing semicolon.<commit_after>(function () {

function Data (data) {
  this.data = data;
}

Data.prototype = {

  bound : function (min, max) {

    if (!_.isNumber(min) || !_.isNumber(max)) return this;

    var
      data    = this.data,
      length  = data.length,
      newData = [],
      i;

    for (i = 0; i < length; i++) {
      if (data[i][0] >= min) break;
    }

    if (i > 0) i--;

    for (i; i < length; i++) {
      newData.push(data[i]);
      if (data[i][0] > max) break;
    }

    this.data = newData;

    return this;
  },
  /**
   * Sample using min and max.
   */
  subsampleMinMax : function (resolution) {

    var
      data    = this.data,
      length  = data.length,
      _length = length - 1,
      newData = [],
      unit    = resolution / length,
      index   = Math.round(unit * 1),
      iMin    = 1,
      iMax    = 1,
      min, max, datum, i, j;

    if (length > resolution) {
      newData.push(data[0]);
      for (i = 1; i < _length; i++) {
        j = Math.round(unit * i);

        // New index
        if (j !== index) {
          if (iMax === iMin) {
            newData.push(data[iMin]);
          } else {
            newData.push(data[Math.min(iMin, iMax)], data[Math.max(iMin, iMax)]);
          }
          iMin = iMax = i;
          min = max = data[i][1];
          index = j;
        }

        // Old index
        else {
          datum = data[i][1];
          if (datum < min) {
            iMin = i;
            min = datum;
          }
          if (datum > max) {
            iMax = i;
            max = datum;
          }
        }
      }
      newData.push(data[length-1]);
      this.data = newData;
    }

    return this;
  },

  subsample : function (resolution) {

    var
      data    = this.data,
      length  = data.length,
      newData = [],
      unit    = length / resolution,
      i;

    if (length > resolution) {
      newData.push(data[0]);
      for (i = 1; i < resolution; i++) {
        if (i * unit >= length)
          break;
        newData.push(data[Math.round(i*unit)]);
      }
      newData.push(data[length-1]);
      this.data = newData;
    }

    return this;
  }
};

humblevis.Data = Data;

}());
<|endoftext|>


<commit_before>var Main = (function(){

    //Constructor
    function Main(){

        // Get new library object for storing elements
        Main.library = new Storage();

        Main.prototype.fetchElements();
        Main.prototype.initializeEventListeners();

        Main.prototype.init();

    }

    //atrributes
    Main.library = "";
    Main.video = null;
    Main.view = null;
    Main.urlVars = null;
    //data -> todo: use Storage library
    Main.aVideosThumbs = []
    Main.aThumbs = []  //array thumbs
    Main.videos = null //videos

    Main.prototype = {

        init: function () {

            //load video if exists Id. else we load it after ask for loadVideosFromChannel.
            if (typeof(Main.urlVars["id"]) != "undefined"){

                //set and return final url
                var videoUrl = 'http://www.vimeo.com/' + Main.urlVars["id"];

                var ov = {
                    videoUrl: videoUrl,
                    videoTiming: 0 //don't use timing at the moment
                }

                Video.prototype.getVideo(ov);
            }

            Main.video = new Video();
            Main.video.loadVideosFromChannel();

        },

        fetchElements: function(){
            var hidedHeader     = $('#hidedHeader')
            var sidebar         = $('#st_thumbs_wrapper')
            var html            = $('html');
            var cWindow         = $(window);
            var divSidebar      = $('.st_thumbs div');

            Main.library.set('hidedHeader', hidedHeader);
            Main.library.set('sidebar', sidebar);

            Main.library.set('html', html);
            Main.library.set('cWindow', cWindow);
            Main.library.set('divSidebar', divSidebar);

        },

        initializeEventListeners: function () {

            /** get Vars from URL */
            Main.urlVars = this.getUrlVars();

            /** document events **/
            var cWindow = Main.library.get('cWindow');
            var html = Main.library.get('html');
            var hidedHeader = Main.library.get('hidedHeader');
            var sidebar = Main.library.get('sidebar');
            var divSidebar = Main.library.get('divSidebar');

            cWindow.resize(Main.prototype.resizeScreenElements);
            divSidebar.hover(Main.prototype.highLight);

            sidebar.hover(Main.prototype.showSidebar, Main.prototype.hideSidebar);
            hidedHeader.hover(Main.prototype.showCloseHeader, Main.prototype.hideCloseHeader);

            /** show iframe **/
            $(".wrapper").fadeIn(2000);

        },

        resizeScreenElements: function(){

            var elem 			= $('body');
            var thumbs_wrapper = elem.find('.st_thumbs_wrapper');
            thumbs_wrapper.css('height', ($(window).height() - 42) + 'px');

            $('.wrapper iframe').css('height', $(window).height() + 'px');
            $('.wrapper iframe').css('width', $(window).width() + 'px');
        },

        highLight: function(){
            $(this).stop().animate({
                opacity: 1
            }, 500)

        },

        showSidebar: function () {
            $(this).stop().animate({
                opacity: 1
            }, 500)
        },

        hideSidebar: function () {
            $(this).stop().animate({
                opacity: 0
            }, 500)
        },

        showCloseHeader: function () {
            $("#closeHeader").stop().animate({"top": "+65px"}, "fast");
        },
        hideCloseHeader: function () {
            $("#closeHeader").stop().animate({"top": "-65px"}, "fast");
        },

        getUrlVars: function(){
            var vars = {};
            var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
                vars[key] = value;
            });

           return vars;
        }

}
    return Main;
}());

<commit_msg>removed unused code in Main<commit_after>var Main = (function(){

    //Constructor
    function Main(){

        // Get new library object for storing elements
        Main.library = new Storage();

        Main.prototype.fetchElements();
        Main.prototype.initializeEventListeners();

        Main.prototype.init();

    }

    //atrributes
    Main.library = "";
    Main.video = null;
    Main.view = null;
    Main.urlVars = null;
    //data -> todo: use Storage library
    Main.aVideosThumbs = []
    Main.aThumbs = []  //array thumbs
    Main.videos = null //videos

    Main.prototype = {

        init: function () {

            //load video if exists Id. else we load it after ask for loadVideosFromChannel.
            if (typeof(Main.urlVars["id"]) != "undefined"){

                //set and return final url
                var videoUrl = 'http://www.vimeo.com/' + Main.urlVars["id"];

                var ov = {
                    videoUrl: videoUrl,
                    videoTiming: 0 //don't use timing at the moment
                }

                Video.prototype.getVideo(ov);
            }

            Main.video = new Video();
            Main.video.loadVideosFromChannel();

        },

        fetchElements: function(){
            var hidedHeader     = $('#hidedHeader')
            var sidebar         = $('#st_thumbs_wrapper')
            var html            = $('html');
            var cWindow         = $(window);

            Main.library.set('hidedHeader', hidedHeader);
            Main.library.set('sidebar', sidebar);

            Main.library.set('html', html);
            Main.library.set('cWindow', cWindow);

        },

        initializeEventListeners: function () {

            /** get Vars from URL */
            Main.urlVars = this.getUrlVars();

            /** document events **/
            var cWindow = Main.library.get('cWindow');
            var html = Main.library.get('html');
            var hidedHeader = Main.library.get('hidedHeader');
            var sidebar = Main.library.get('sidebar');

            cWindow.resize(Main.prototype.resizeScreenElements);

            sidebar.hover(Main.prototype.showSidebar, Main.prototype.hideSidebar);
            hidedHeader.hover(Main.prototype.showCloseHeader, Main.prototype.hideCloseHeader);

            /** show iframe **/
            $(".wrapper").fadeIn(2000);

        },

        resizeScreenElements: function(){

            var elem 			= $('body');
            var thumbs_wrapper = elem.find('.st_thumbs_wrapper');
            thumbs_wrapper.css('height', ($(window).height() - 42) + 'px');

            $('.wrapper iframe').css('height', $(window).height() + 'px');
            $('.wrapper iframe').css('width', $(window).width() + 'px');
        },

        showSidebar: function () {
            $(this).stop().animate({
                opacity: 1
            }, 500)
        },

        hideSidebar: function () {
            $(this).stop().animate({
                opacity: 0
            }, 500)
        },

        showCloseHeader: function () {
            $("#closeHeader").stop().animate({"top": "+65px"}, "fast");
        },
        hideCloseHeader: function () {
            $("#closeHeader").stop().animate({"top": "-65px"}, "fast");
        },

        getUrlVars: function(){
            var vars = {};
            var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
                vars[key] = value;
            });

           return vars;
        }

}
    return Main;
}());

<|endoftext|>


<commit_before>app.factory('BioFactory', ['$http', function($http){
  console.log('BioFactory running');

  var mentors = {};
  var mentorBio = {};
  var mentorFAQs = [];
  var mentorId;

  // gets all the mentors that match the newSearch selected fields
  function getMentors(newSearch){
    console.log("NEW SEARCH:", newSearch);
    var newSearchString = JSON.stringify(newSearch);
    return $http({
        method: 'GET',
        url: '/mentor-search/search',
        headers: {
          newSearchString: newSearchString
        }
      })
      .then(function(response) {
        mentors.info = response.data;
        console.log("Mentors list:", mentors);
      }),
      function(err) {
        console.log("Error with search get request ", err);
      };
  }

  // gets profiles associated with the mentor

  function getProfiles(){
    console.log("MENTOR ID IN getProfiles()", mentorId);
    return $http.get('/profile/' + mentorId)
    .then(function (response) {

      mentorBio = {
        id: response.data[0].id,
        first_name: response.data[0].first_name,
        last_name: response.data[0].last_name,
        email: response.data[0].email,
        avatar: response.data[0].avatar,
        company: response.data[0].company,
        job_title: response.data[0].job_title,
        zip: response.data[0].zip,
        race: response.data[0].race,
        gender: response.data[0].gender,
        orientation: response.data[0].orientation,
        birthday: response.data[0].birthday,
        school: response.data[0].school,
        degree: response.data[0].degree,
        major: response.data[0].major,
        languages: response.data[0].languages
      };

      for(var key in response.data) {
        mentorFAQs.push(
          {
            question: response.data[key].question,
            answer: response.data[key].answer
          }
        );
      }

    })
    .catch(function (error) {
      console.log('An error has occurred');
    });
  }

// Attach the chosen mentor object to the info property on mentorBio.
// This will be accessed by the profile controller
  function setMentor(mentor){
    mentorBio.info = mentor;
    mentorId = mentorBio.info.id;
    console.log("MENTOR:", mentorBio.info);
  }

  function setMentorId(id){
    mentorId = id;
    console.log('MENTOR ID:', mentorId);
  }

  var publicApi = {
    mentors: mentors,
    mentorBio: mentorBio,
    mentorFAQs: mentorFAQs,
    getMentors: function(newSearch){
      return getMentors(newSearch);
    },
    setMentor: function(mentor){
      return setMentor(mentor);
    },
    getProfiles: function(){
      return getProfiles();
    },
    setMentorId: function(id){
      return setMentorId(id);
    }
    // getMentorId: function(){
    //   return getMentorId();
    // }
  };

  return publicApi;

}]);
<commit_msg>Line up code to make it more pretty<commit_after>app.factory('BioFactory', ['$http', function($http){
  console.log('BioFactory running');

  var mentors = {};
  var mentorBio = {};
  var mentorFAQs = [];
  var mentorId;

  // gets all the mentors that match the newSearch selected fields
  function getMentors(newSearch){
    console.log("NEW SEARCH:", newSearch);
    var newSearchString = JSON.stringify(newSearch);
    return $http({
        method: 'GET',
        url: '/mentor-search/search',
        headers: {
          newSearchString: newSearchString
        }
      })
      .then(function(response) {
        mentors.info = response.data;
        console.log("Mentors list:", mentors);
      }),
      function(err) {
        console.log("Error with search get request ", err);
      };
  }

  // gets profiles associated with the mentor

  function getProfiles(){
    console.log("MENTOR ID IN getProfiles()", mentorId);
    return $http.get('/profile/' + mentorId)
    .then(function (response) {

      mentorBio = {
        id:           response.data[0].id,
        first_name:   response.data[0].first_name,
        last_name:    response.data[0].last_name,
        email:        response.data[0].email,
        avatar:       response.data[0].avatar,
        company:      response.data[0].company,
        job_title:    response.data[0].job_title,
        zip:          response.data[0].zip,
        race:         response.data[0].race,
        gender:       response.data[0].gender,
        orientation:  response.data[0].orientation,
        birthday:     response.data[0].birthday,
        school:       response.data[0].school,
        degree:       response.data[0].degree,
        major:        response.data[0].major,
        languages:    response.data[0].languages
      };

      for(var key in response.data) {
        mentorFAQs.push(
          {
            question:   response.data[key].question,
            answer:     response.data[key].answer
          }
        );
      }

    })
    .catch(function (error) {
      console.log('An error has occurred');
    });
  }

// Attach the chosen mentor object to the info property on mentorBio.
// This will be accessed by the profile controller
  function setMentor(mentor){
    mentorBio.info = mentor;
    mentorId = mentorBio.info.id;
    console.log("MENTOR:", mentorBio.info);
  }

  function setMentorId(id){
    mentorId = id;
    console.log('MENTOR ID:', mentorId);
  }

  var publicApi = {
    mentors: mentors,
    mentorBio: mentorBio,
    mentorFAQs: mentorFAQs,
    getMentors: function(newSearch){
      return getMentors(newSearch);
    },
    setMentor: function(mentor){
      return setMentor(mentor);
    },
    getProfiles: function(){
      return getProfiles();
    },
    setMentorId: function(id){
      return setMentorId(id);
    }
    // getMentorId: function(){
    //   return getMentorId();
    // }
  };

  return publicApi;

}]);
<|endoftext|>


<commit_before>/* global requirejs,define,window,console */
requirejs.config({
    //默认从 js/ 中加载模块
    baseUrl: 'js/',

    paths: {
        text: 'thirdLib/requirejs/text',
        GlobalVar: 'module/GlobalVar',
        MainFrame: 'module/MainFrame',
        THREE: 'thirdLib/threejs/Three.min',
    },
    shim: {
        THREE: {
            exports: 'THREE'
        }
    }
});

define(function (require) {
    'use strict';

    var Context = require('module/Context'),
        CssJsLoader = require('module/CssJsLoader');

    var mainFrame = null;

    require(['MainFrame'], function (MainFrame) {
        console.log('Editor Run');

        mainFrame = new MainFrame();

        loadCssFiles();

    });

    function loadCssFiles() {
        var fileNames = [
            'css/pure-min.css',
            'css/grids-min.css',
            'css/menus-min.css',
            'css/grids-responsive-min.css'
        ];

        fileNames.forEach(function (fileName) {
            CssJsLoader.loadCssFile(Context.getServerUrl() + fileName);
        });
    }

    window.onbeforeunload = function () {
        return 'Are you sure want to exit';
    };

    window.onresize = function () {

        if (null !== mainFrame) {
            mainFrame.onWindowResize();
        }
    };
});<commit_msg>globalvar do not use global path<commit_after>/* global requirejs,define,window,console */
requirejs.config({
    //默认从 js/ 中加载模块
    baseUrl: 'js/',

    paths: {
        text: 'thirdLib/requirejs/text',
        MainFrame: 'module/MainFrame',
        THREE: 'thirdLib/threejs/Three.min',
    },
    shim: {
        THREE: {
            exports: 'THREE'
        }
    }
});

define(function (require) {
    'use strict';

    var Context = require('module/Context'),
        CssJsLoader = require('module/CssJsLoader');

    var mainFrame = null;

    require(['MainFrame'], function (MainFrame) {
        console.log('Editor Run');

        mainFrame = new MainFrame();

        loadCssFiles();

    });

    function loadCssFiles() {
        var fileNames = [
            'css/pure-min.css',
            'css/grids-min.css',
            'css/menus-min.css',
            'css/grids-responsive-min.css'
        ];

        fileNames.forEach(function (fileName) {
            CssJsLoader.loadCssFile(Context.getServerUrl() + fileName);
        });
    }

    window.onbeforeunload = function () {
        return 'Are you sure want to exit';
    };

    window.onresize = function () {

        if (null !== mainFrame) {
            mainFrame.onWindowResize();
        }
    };
});<|endoftext|>


<commit_before>'use strict';

//  ---------------------------------------------------------------------------

const Exchange = require ('./base/Exchange');
const { ExchangeError } = require ('./base/errors');

//  ---------------------------------------------------------------------------

module.exports = class bcex extends Exchange {
    describe () {
        return this.deepExtend (super.describe (), {
            'id': 'bcex',
            'name': 'bcex',
            'countries': [ 'CA' ],
            'rateLimit': 500,
            'version': '1',
            'has': {
                'fetchBalance': true,
                'fetchMarkets': true,
                'createOrder': true,
                'cancelOrder': true,
                'fetchTicker': true,
                'fetchTickers': false,
                'fetchOrder': true,
                'fetchOrders': true,
                'fetchOpenOrders': true,
            },
            'urls': {
                'logo': 'https://www.bcex.top/images/bcex_logo0.png',
                'api': 'https://www.bcex.top',
                'www': 'https://www.bcex.ca',
                'doc': 'https://www.bcex.ca/api_market/market/',
                'fees': 'http://bcex.udesk.cn/hc/articles/57085',
            },
            'api': {
                'public': {
                    'get': [
                        'Api_Market/getPriceList'
                    ],
                },
                'private': {
                    'post': [
                        'Api_User/userBalance',
                        'Api_Order/coinTrust',
                        'Api_Order/cancel',
                        'Api_Order/ticker',
                        'Api_Order/orderList',
                        'Api_Order/tradeList',
                    ],
                },
            },
            'fees': {
            },
        });
    }

    async fetchMarkets () {
        let response = await this.publicGetApiMarketGetPriceList ();
        let result = [];
        let keys = Object.keys(response);
        for (let i = 0; i < keys.length; i++) {
            var currentMarketId = keys[i];
            var currentMarkets = response[currentMarketId];
            for (let j = 0; j < currentMarkets.length; j++) {
                var market = currentMarkets[j];
                let baseId = market.coin_from;
                let quoteId = market.coin_to;
                let base = this.commonCurrencyCode(baseId);
                let quote = this.commonCurrencyCode(quoteId);
                let id = base + "2" + quote;
                let symbol = base + '/' + quote;
                let active = true;
                result.push({
                    'id': id,
                    'symbol': symbol,
                    'base': base,
                    'quote': quote,
                    'baseId': baseId,
                    'quoteId': quoteId,
                    'active': active,
                    'info': market
                    });
            }
        }
        return result;
    }

    async fetchBalance (params = {}) {
        await this.loadMarkets ();
        params['api_key'] = this.apiKey
        let response = await this.privatePostApiUserUserBalance (params)
        let data = response['data'];
        let keys = Object.keys(data);
        let result = { }
        for (let i = 0; i < keys.length; i++) {
            let currentKey = keys[i];
            let currentAmount = data[currentKey];
            let split = currentKey.split('_')
            let id = split[0];
            let lockOrOver = split[1];
            let currency = this.commonCurrencyCode(id);
            if (!(currency in result)) {
                let account = this.account ()
                result[currency] = account;
            }
            if (lockOrOver == 'lock') {
                result[currency]['used'] = parseFloat(currentAmount);
            } else {
                result[currency]['free'] = parseFloat(currentAmount);
            }
        }
        keys = Object.keys(result)
        for (let i = 0; i < keys.length; i++) {
            let currentKey = keys[i];
            let x = result[currentKey]
            let total = result[currentKey]['used'] + result[currentKey]['total']
            result[currentKey]['total'] = total;
        }
        result['info'] = data
        return this.parseBalance(result);
    }

    async fetchOrderBook (symbol, limit = undefined, params = {}) {
        return 0;
    }

    async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
        return 0;
    }

    async fetchOrder (id, symbol = undefined, params = {}) {
        return 0;
    }

    async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
        return 0;
    }

    async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
        return 0;
    }

    async cancelOrder (id, symbol = undefined, params = {}) {

        await this.loadMarkets();
        let request = {
            'order_id': id,
            'symbol' : symbol
        }

        let response = await this.privatePostApiOrderCancel(this.extend(request, params));

        return 0;
    }

    sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
        let request = '/' + this.implodeParams(path, params);
        let query = this.omit(params, this.extractParams(path));
        if (method === 'GET') {
            if (Object.keys(query).length)
                request += '?' + this.urlencode(query);
        }
        let url = this.urls['api'] + request;
        if (api === 'private') {
            this.checkRequiredCredentials();
            if (method !== 'GET') {
                if (Object.keys(query).length) {
                    let messageParts = []
                    let paramsKeys = Object.keys(params).sort();
                    for (let i = 0; i < paramsKeys.length; i++) {
                        let paramKey = paramsKeys[i];
                        let param = params[paramKey];
                        messageParts.push(this.encode(paramKey) + '=' + encodeURIComponent(param));
                    }
                    body = messageParts.join ('&');
                    let message = body + "&secret_key=" + this.secret;
                    let signedMessage = this.hash(message);
                    body = body + "&sign=" + signedMessage;
                    params['sign'] = signedMessage;
                    headers = {}
                    headers['Content-Type'] = 'application/x-www-form-urlencoded';
                }
            }
        }
        return { 'url': url, 'method': method, 'body': body, 'headers': headers };
    }
};
<commit_msg>Implement fetch open orders<commit_after>'use strict';

//  ---------------------------------------------------------------------------

const Exchange = require ('./base/Exchange');
const { ExchangeError } = require ('./base/errors');

//  ---------------------------------------------------------------------------

module.exports = class bcex extends Exchange {
    describe () {
        return this.deepExtend (super.describe (), {
            'id': 'bcex',
            'name': 'bcex',
            'countries': [ 'CA' ],
            'rateLimit': 500,
            'version': '1',
            'has': {
                'fetchBalance': true,
                'fetchMarkets': true,
                'createOrder': true,
                'cancelOrder': true,
                'fetchTicker': true,
                'fetchTickers': false,
                'fetchOrder': true,
                'fetchOrders': true,
                'fetchOpenOrders': true,
            },
            'urls': {
                'logo': 'https://www.bcex.top/images/bcex_logo0.png',
                'api': 'https://www.bcex.top',
                'www': 'https://www.bcex.ca',
                'doc': 'https://www.bcex.ca/api_market/market/',
                'fees': 'http://bcex.udesk.cn/hc/articles/57085',
            },
            'api': {
                'public': {
                    'get': [
                        'Api_Market/getPriceList'
                    ],
                },
                'private': {
                    'post': [
                        'Api_User/userBalance',
                        'Api_Order/coinTrust',
                        'Api_Order/cancel',
                        'Api_Order/ticker',
                        'Api_Order/orderList',
                        'Api_Order/tradeList',
                    ],
                },
            },
            'fees': {
            },
        });
    }

    async fetchMarkets () {
        let response = await this.publicGetApiMarketGetPriceList ();
        let result = [];
        let keys = Object.keys(response);
        for (let i = 0; i < keys.length; i++) {
            var currentMarketId = keys[i];
            var currentMarkets = response[currentMarketId];
            for (let j = 0; j < currentMarkets.length; j++) {
                var market = currentMarkets[j];
                let baseId = market.coin_from;
                let quoteId = market.coin_to;
                let base = this.commonCurrencyCode(baseId);
                let quote = this.commonCurrencyCode(quoteId);
                let id = base + "2" + quote;
                let symbol = base + '/' + quote;
                let active = true;
                result.push({
                    'id': id,
                    'symbol': symbol,
                    'base': base,
                    'quote': quote,
                    'baseId': baseId,
                    'quoteId': quoteId,
                    'active': active,
                    'info': market
                    });
            }
        }
        return result;
    }

    async fetchBalance (params = {}) {
        await this.loadMarkets ();
        params['api_key'] = this.apiKey
        let response = await this.privatePostApiUserUserBalance (params)
        let data = response['data'];
        let keys = Object.keys(data);
        let result = { }
        for (let i = 0; i < keys.length; i++) {
            let currentKey = keys[i];
            let currentAmount = data[currentKey];
            let split = currentKey.split('_')
            let id = split[0];
            let lockOrOver = split[1];
            let currency = this.commonCurrencyCode(id);
            if (!(currency in result)) {
                let account = this.account ()
                result[currency] = account;
            }
            if (lockOrOver == 'lock') {
                result[currency]['used'] = parseFloat(currentAmount);
            } else {
                result[currency]['free'] = parseFloat(currentAmount);
            }
        }
        keys = Object.keys(result)
        for (let i = 0; i < keys.length; i++) {
            let currentKey = keys[i];
            let total = result[currentKey]['used'] + result[currentKey]['total']
            result[currentKey]['total'] = total;
        }
        result['info'] = data
        return this.parseBalance(result);
    }

    async fetchOrderBook (symbol, limit = undefined, params = {}) {
        return 0;
    }

    async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
        return 0;
    }

    async fetchOrder (id, symbol = undefined, params = {}) {
        return 0;
    }

    parseOrder (order, market = undefined) {
        let id = order['id'].toString ();
        let timestamp = order['datetime'] * 1000;
        let iso8601 = this.iso8601 (timestamp);
        let symbol = market['symbol'];
        let type = order['type'];
        let side = "TODO";
        let price = order['price']
        let average = order['avg_price']
        let amount = order['amount']
        let remaining = order['amount_outstanding']
        let filled = amount - remaining;
        let status = order['status']
        let cost = filled * price;
        let fee = undefined;
        if ('fee' in order) {
            fee = {
                'cost': parseFloat (order['fee']),
                'currency': this.commonCurrencyCode (order['feeCurrency']),
            };
        }
        let result = {
            'info': order,
            'id': id,
            'timestamp': timestamp,
            'datetime': iso8601,
            'lastTradeTimestamp': undefined,
            'symbol': symbol,
            'type': type,
            'side': side,
            'price': price,
            'cost': cost,
            'average': undefined,
            'amount': amount,
            'filled': filled,
            'remaining': remaining,
            'status': status,
            'fee': fee,
        };
        return result;
    }

    async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
        await this.loadMarkets();
        let request = {};
        request['api_key'] = this.apiKey;
        if (typeof symbol !== 'undefined') {
            request['symbol'] = symbol;
            request['type'] = 'open';
        }
        let response = await this.privatePostApiOrderTradeList(this.extend(request, params));
        let market = undefined;
        if (symbol in this.markets_by_id) {
            market = this.markets_by_id[symbol];
        }
        let results = this.parseOrders(response['data'], market, since, limit);
        return results;
    }

    async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
        return 0;
    }

    async cancelOrder (id, symbol = undefined, params = {}) {
        return 0;
    }

    sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
        let request = '/' + this.implodeParams(path, params);
        let query = this.omit(params, this.extractParams(path));
        if (method === 'GET') {
            if (Object.keys(query).length)
                request += '?' + this.urlencode(query);
        }
        let url = this.urls['api'] + request;
        if (api === 'private') {
            this.checkRequiredCredentials();
            if (method !== 'GET') {
                if (Object.keys(query).length) {
                    let messageParts = []
                    let paramsKeys = Object.keys(params).sort();
                    for (let i = 0; i < paramsKeys.length; i++) {
                        let paramKey = paramsKeys[i];
                        let param = params[paramKey];
                        messageParts.push(this.encode(paramKey) + '=' + encodeURIComponent(param));
                    }
                    body = messageParts.join ('&');
                    let message = body + "&secret_key=" + this.secret;
                    let signedMessage = this.hash(message);
                    body = body + "&sign=" + signedMessage;
                    params['sign'] = signedMessage;
                    headers = {}
                    headers['Content-Type'] = 'application/x-www-form-urlencoded';
                }
            }
        }
        return { 'url': url, 'method': method, 'body': body, 'headers': headers };
    }
};
<|endoftext|>