<commit_before>/**
 * @TODO Document.
 */

/* globals jQuery: false, document: false, io: false, location: false, console: false, window: false, history: false, url: false */

'use strict'; // jshint ignore: line

// TODO:
//  grab on submit and cancel, get values
//  client-side protocol
//  show errors

jQuery.extend({
    getQueryParameters : function(str) {
        return (str || document.location.search)
            .replace(/(^\?)/,'')
            .split("&")
            .map(function(n){
                return n = n.split("="), this[n[0]] = (n[1] === "true" || n[1] === "false") ? JSON.parse(n[1]) : n[1], this;
            }
            .bind({}))[0];
    }
});

(function ($) {

    const MSG_EXCEPTION = {ui: 'Bug', bootstrap: 'danger'}
    ,   MSG_ERROR = {ui: 'Error', bootstrap: 'danger'}
    ,   MSG_WARN = {ui: 'Warning', bootstrap: 'warning'}
    ,   MSG_INFO = {ui: 'Advice', bootstrap: 'info'}
    ;

    var $form = $("form#options")
    ,   $urlContainer = $("#urlContainer")
    ,   $url = $("#url")
    ,   profile
    ,   $profileContainer = $("#profileContainer")
    ,   $profile = $("#profile")
    ,   $profileOptions = $('#profile option')
    ,   $validation = $("#validation")
    ,   $noRecTrack = $("#noRecTrack")
    ,   $informativeOnly = $("#informativeOnly")
    ,   $echidnaReady = $("#echidnaReady")
    ,   $patentPolicy = $("#patentPolicy")
    ,   $results = $('#results')
    ,   $resultsBody = $results.find('.panel-body')
    ,   $resultsList = $results.find('.list-group')
    ,   $progressContainer = $("#progressBar")
    ,   $progress = $progressContainer.find(".progress-bar")
    ,   $progressStyler = $progress.parent()
    ,   baseURI = (location.pathname + '/').replace(/\/+$/, '/')
    ,   socket = io(location.protocol + "//" + location.host, {path: baseURI + 'socket.io'})
    ,   done = 0
    ,   result = {exceptions: [], errors: [], warnings: [], infos: []}
    ,   total = 0
    ;

    // handshake
    socket.on("handshake", function (data) {
        console.log(`Handshake; using version “${data.version}”.`);
    });

    // validate
    function validate (options) {
        $resultsBody.empty();
        $resultsList.empty();
        profile = options.profile;
        socket.emit("validate", {
            url:                decodeURIComponent(options.url)
        ,   profile:            profile
        ,   validation:         options.validation
        ,   noRecTrack:         (true === options.noRecTrack)
        ,   informativeOnly:    (true === options.informativeOnly)
        ,   echidnaReady:       (true === options.echidnaReady)
        ,   patentPolicy:       options.patentPolicy
        });
    }

    // extractMetadata
    function extractMetadata (url) {
        socket.emit("extractMetadata", {
            url: decodeURIComponent(url)
        });
    }

    function addMessage (type, data) {
        const url = $url.val();
        var inContext = ''
        ,   issue
        ,   exc = (data && data.exception) ? ' exception' : ''
        ;
        if (data && data.id) {
            const newProfile = profile.replace(/\-echidna$/i, '');
            inContext = `<a href="doc/rules?profile=${newProfile}#${data.id}">See rule in context</a> <br>`;
        }
        if (data && data.name)
            issue = `<a href="https://github.com/w3c/specberus/issues/new?` +
                `title=Bug%20in%20rule%20%E2%80%9C${data.name}%E2%80%9D:%20[WHAT]&` +
                `body=[EXPLANATION]%0A%0AFound%20[while%20checking%20\`${url}\`](${encodeURIComponent(window.location)}).&` +
                `labels=from-template` +
                `">Report a bug</a>`;
        else if (data && data.exception)
            issue = `<a href="https://github.com/w3c/specberus/issues/new?` +
                `title=Bug:%20[WHAT]&` +
                `body=[EXPLANATION]%0A%0AFound%20[while%20checking%20\`${url}\`](${encodeURIComponent(window.location)}).&` +
                `labels=from-template` +
                `">Report a bug</a>`;
        else
            issue = `<a href="https://github.com/w3c/specberus/issues/new?` +
                `title=Bug%20in%20rules:%20[WHAT]&` +
                `body=[EXPLANATION]%0A%0AFound%20[while%20checking%20\`${url}\`](${encodeURIComponent(window.location)}).&` +
                `labels=from-template` +
                `">Report a bug</a>`;
        var item = `

            ${type.ui}

 ${inContext} ${issue}

            ${data.message}

`;
        if (MSG_EXCEPTION === type) result.exceptions.push(item);
        else if (MSG_ERROR === type) result.errors.push(item);
        else if (MSG_WARN === type) result.warnings.push(item);
        else if (MSG_INFO === type) result.infos.push(item);
    }

    function toggleManual(toggle) {
        $form.toggleClass('manual', toggle);
        $urlContainer.toggleClass('col-sm-4', !toggle);
        $urlContainer.toggleClass('col-sm-5', toggle);
        $profileContainer.toggleClass('col-sm-4 col-md-4 col-lg-4', !toggle);
        $profileContainer.toggleClass('col-sm-5 col-md-5 col-lg-5', toggle);
    }

    socket.on("exception", function (data) {
        const message = data.message ? data.message : data;
        addMessage(MSG_EXCEPTION, {message: `<div class="message">${message}</div>`, exception: true});
        window.setTimeout(showResults, 1000);
    });

    socket.on("start", function (data) {
        console.log('Started.');
        done = 0;
        result = {exceptions: [], errors: [], warnings: [], infos: []};
        total = data.rules.length;
        $progressStyler.addClass("active progress-striped");
        $progressContainer.fadeIn();
        $results.hide();
    });
    socket.on('err', function (data) {
        addMessage(MSG_ERROR, data);
    });
    socket.on('warning', function (data) {
        addMessage(MSG_WARN, data);
    });
    socket.on('info', function (data) {
        addMessage(MSG_INFO, data);
    });
    socket.on("done", function () {
        done++;
        $progress.attr({
            "aria-valuenow":    done
        ,   "aria-valuemax":    total
        ,   "style":            "width: " + (total ? (done/total)*100 : 0) + "%"
        });
        $progress.text(done + '/' + total);
    });
    socket.on("finished", function () {
        console.log('Finished.');
        showResults();
    });
    socket.on("finishedExtraction", function (data) {
        if (!data || !data.metadata || !data.metadata.profile) {
            addMessage(MSG_EXCEPTION, {message: `<div class="message">Metadata extraction could not infer a profile for the document.</div>`});
            window.setTimeout(showResults, 1000);
        }
        else {
            toggleManual(true);
            profile = data.metadata.profile;
            $profile.val(profile);
            $noRecTrack.prop('checked', !data.metadata.rectrack);
            $informativeOnly.prop('checked', data.metadata.informative);
            $validation.find("label").removeClass('active');
            $validation.find('label#simple-validation').addClass('active');
            var options = {
                              "url"             : data.url
                            , "profile"         : profile
                            , "validation"      : 'simple-validation'
                            , "noRecTrack"      : !data.metadata.rectrack || false
                            , "informativeOnly" : data.metadata.informative || false
                            , "echidnaReady"    : false
                            , "patentPolicy"    : "pp2004"
                          };
            validate(options);
            var newurl = document.URL.split('?')[0] + "?" + $.param(options);
            history.pushState(options, url + " - " + profile, newurl);
        }
    });

    $form.submit(function () {
        $resultsBody.empty();
        $resultsList.empty();
        result = {exceptions: [], errors: [], warnings: [], infos: []};
        if ($profile.val() === "auto") {
            extractMetadata($url.val());
        } else {
            var url = $url.val()
            ,   validation = $validation.find('label.active').attr('id')
            ,   noRecTrack = $noRecTrack.is(":checked") || false
            ,   informativeOnly = $informativeOnly.is(":checked") || false
            ,   echidnaReady = $echidnaReady.is(":checked") || false
            ,   patentPolicy = $patentPolicy.find('label.active').attr('id')
            ;
            profile = $profile.val();
            if (!url) addMessage(MSG_ERROR, 'Missing “URL” parameer');
            if (!profile) addMessage(MSG_ERROR, 'Missing “profile” parameter');
            if (echidnaReady) profile += '-Echidna';
            var options = {
                              "url"             : url
                            , "profile"         : profile
                            , "validation"      : validation
                            , "noRecTrack"      : noRecTrack
                            , "informativeOnly" : informativeOnly
                            , "echidnaReady"    : echidnaReady
                            , "patentPolicy"    : patentPolicy
                          };
            validate(options);
            var newurl = document.URL.split('?')[0] + "?" + $.param(options);
            history.pushState(options, url + " - " + profile, newurl);
        }
        return false;
    });

    function disableProfilesIfNeeded(checkbox) {
        if (checkbox.prop('checked')) {
            $profileOptions.each(function (_, el) {
                if ($(el).val() !== 'WD' && $(el).val() !== 'WG-NOTE')
                  $(el).prop('disabled', true);
            });
            if ($profile.val() !== 'WD' && $profile.val() !== 'WG-NOTE')
              $profile.val('');
        }
        else $profileOptions.each(function (_, el) {
            if ($(el).val() !== '') $(el).prop('disabled', false);
        });
    }

    function setFormParams(options) {
        // Option "echidnaReady" processed first, as it may restrict the list of enabled profiles.
        $echidnaReady.prop('checked', options.echidnaReady);
        if (options.url) $url.val(decodeURIComponent(options.url));
        // "profile" might be eg "WD-Echidna". Normalise.
        if (options.profile) {
          const newProfile = options.profile.replace(/\-echidna$/i, '');
          $profile.find('option[value=' + newProfile + ']').prop('selected', true);
        }
        if (options.validation) {
          $validation.find('label#' + options.validation).addClass('active');
          $validation.find(':not(label#' + options.validation + ')').removeClass('active');
        }
        $noRecTrack.prop('checked', options.noRecTrack);
        $informativeOnly.prop('checked', options.informativeOnly);
        if (options.patentPolicy) {
          $patentPolicy.find('label').removeClass('active');
          $patentPolicy.find('label#' + options.patentPolicy).addClass('active');
        }
    }

    const countNicely = function(term, no) {
        if (0 === no)
            return 'No ' + term + 's';
        else if (1 === no)
            return 'One ' + term;
        else
            return no + ' ' + term + 's';
    };

    function showResults() {
        $results.fadeIn();
        $progressStyler.removeClass("active progress-striped");
        $progressContainer.hide();
        $progress.text('Done!');
        var message;
        if (result.errors.length > 0 || result.exceptions.length > 0) {
            message = `<span class="icon red pull-left">✗</span>`;
            if (result.warnings.length > 0)
                message += `
${countNicely('error', result.exceptions.length + result.errors.length)}
                    (and ${countNicely('warning', result.warnings.length)})
`;
            else
                message += `<h4>${countNicely('error', result.exceptions.length + result.errors.length)}</h4>`;
        }
        else {
            if (result.warnings.length > 0)
                message = `✓

All tests passed, but you are strongly encouraged to address
                    ${countNicely('warning', result.warnings.length).toLowerCase()} before publishing.
`;
            else
                message = `✓

OK!
`;
        }
        message += `<p class="details">`;
        if (total > 0 && profile)
            message += `<a href="doc/rules?profile=${profile}">${total} rules</a> were checked. `;
        message += `Hover over the rows below for options.<p>`;
        $resultsBody.html(message);
        message = '';
        if (result.exceptions.length > 0 || result.errors.length > 0 || result.warnings.length > 0 || result.infos.length > 0) {
            var i;
            for (i in result.exceptions) {
                message += result.exceptions[i];
            }
            for (i in result.errors) {
                message += result.errors[i];
            }
            for (i in result.warnings) {
                message += result.warnings[i];
            }
            for (i in result.infos) {
                message += result.infos[i];
            }
        }
        $resultsList.html(message);
    }

    window.addEventListener('popstate', function(event) {
        var options = event.state;
        if (null === options) return;
        setFormParams(options);
        validate(options);
    });

    $patentPolicy.find("label").on("click", function() {
        var isPP2002 = ($(this).attr("id") === "pp2002");
        $noRecTrack.prop("disabled", isPP2002);
        $informativeOnly.prop("disabled", isPP2002);
    });

    $profile.change(function() {
        toggleManual('auto' !== $(this).val());
    });

    $echidnaReady.change(function () {
        disableProfilesIfNeeded($echidnaReady);
    });

    $(document).ready(function() {
        $.getJSON(`${baseURI}data/profiles.json`, function(data) {
            $profile.append($('<option value="auto" selected="selected">Auto-detect</option>'));
            $.each(data.tracks, function(foo, track) {
                var optgroup = $('<optgroup label="' + track.name + '"></optgroup>');
                $.each(track.profiles, function(bar, profile) {
                    var option = $('<option value="' + profile.id + '">' + profile.id + ' — ' + profile.name + '</option>');
                    optgroup.append(option);
                });
                $profile.append(optgroup);
            });
            $profileOptions = $('#profile option');
            var options = $.getQueryParameters();
            setFormParams(options);
            toggleManual('auto' !== $profile.val());
            if (options.url && options.profile) {
              if (options.profile === "auto") {
                extractMetadata(options.url);
              } else {
                validate(options);
              }
            }
            $('[data-toggle="tooltip"]').tooltip();
            $url.select();
        });
    });

}(jQuery));
<commit_msg>Monitor socket status; better UI while running<commit_after>/**
 * @TODO Document.
 */

/* globals jQuery: false, document: false, io: false, location: false, console: false, window: false, history: false, url: false */

'use strict'; // jshint ignore: line

// TODO:
//  grab on submit and cancel, get values
//  client-side protocol
//  show errors

jQuery.extend({
    getQueryParameters : function(str) {
        return (str || document.location.search)
            .replace(/(^\?)/,'')
            .split("&")
            .map(function(n){
                return n = n.split("="), this[n[0]] = (n[1] === "true" || n[1] === "false") ? JSON.parse(n[1]) : n[1], this;
            }
            .bind({}))[0];
    }
});

(function ($) {

    const MSG_EXCEPTION = {ui: 'Bug', bootstrap: 'danger'}
    ,   MSG_ERROR = {ui: 'Error', bootstrap: 'danger'}
    ,   MSG_WARN = {ui: 'Warning', bootstrap: 'warning'}
    ,   MSG_INFO = {ui: 'Advice', bootstrap: 'info'}
    ;

    var $form = $("form#options")
    ,   $urlContainer = $("#urlContainer")
    ,   $url = $("#url")
    ,   profile
    ,   $profileContainer = $("#profileContainer")
    ,   $profile = $("#profile")
    ,   $profileOptions = $('#profile option')
    ,   $validation = $("#validation")
    ,   $noRecTrack = $("#noRecTrack")
    ,   $informativeOnly = $("#informativeOnly")
    ,   $echidnaReady = $("#echidnaReady")
    ,   $patentPolicy = $("#patentPolicy")
    ,   $results = $('#results')
    ,   $resultsBody = $results.find('.panel-body')
    ,   $resultsList = $results.find('.list-group')
    ,   $progressContainer = $("#progressBar")
    ,   $progress = $progressContainer.find(".progress-bar")
    ,   $progressStyler = $progress.parent()
    ,   baseURI = (location.pathname + '/').replace(/\/+$/, '/')
    ,   socket = io(location.protocol + "//" + location.host, {path: baseURI + 'socket.io'})
    ,   done = 0
    ,   result = {exceptions: [], errors: [], warnings: [], infos: []}
    ,   total = 0
    ,   running = false
    ;

    // handshake
    socket.on("handshake", function (data) {
        console.log(`Handshake; using version “${data.version}”.`);

        socket.on('disconnect', () => {
            socket.close();
            toggleForm(false);
            window.location.href = window.location.href;
        });

    });

    const toggleForm = (bool) => {
        if (bool) {
            $('form').css('opacity', 1);
            $('form input').removeClass('disabled').removeAttr('disabled');
            $('button[type=submit]').text('Check');
        } else {
            $('form').css('opacity', 0.25);
            $('form input').addClass('disabled').attr('disabled', 'disabled');
            $('button[type=submit]').text('Wait…');
        }
    };

    // validate
    function validate (options) {
        $resultsBody.empty();
        $resultsList.empty();
        profile = options.profile;
        socket.emit("validate", {
            url:                decodeURIComponent(options.url)
        ,   profile:            profile
        ,   validation:         options.validation
        ,   noRecTrack:         (true === options.noRecTrack)
        ,   informativeOnly:    (true === options.informativeOnly)
        ,   echidnaReady:       (true === options.echidnaReady)
        ,   patentPolicy:       options.patentPolicy
        });
    }

    // extractMetadata
    function extractMetadata (url) {
        socket.emit("extractMetadata", {
            url: decodeURIComponent(url)
        });
    }

    function addMessage (type, data) {
        const url = $url.val();
        var inContext = ''
        ,   issue
        ,   exc = (data && data.exception) ? ' exception' : ''
        ;
        if (data && data.id) {
            const newProfile = profile.replace(/\-echidna$/i, '');
            inContext = `<a href="doc/rules?profile=${newProfile}#${data.id}">See rule in context</a> <br>`;
        }
        if (data && data.name)
            issue = `<a href="https://github.com/w3c/specberus/issues/new?` +
                `title=Bug%20in%20rule%20%E2%80%9C${data.name}%E2%80%9D:%20[WHAT]&` +
                `body=[EXPLANATION]%0A%0AFound%20[while%20checking%20\`${url}\`](${encodeURIComponent(window.location)}).&` +
                `labels=from-template` +
                `">Report a bug</a>`;
        else if (data && data.exception)
            issue = `<a href="https://github.com/w3c/specberus/issues/new?` +
                `title=Bug:%20[WHAT]&` +
                `body=[EXPLANATION]%0A%0AFound%20[while%20checking%20\`${url}\`](${encodeURIComponent(window.location)}).&` +
                `labels=from-template` +
                `">Report a bug</a>`;
        else
            issue = `<a href="https://github.com/w3c/specberus/issues/new?` +
                `title=Bug%20in%20rules:%20[WHAT]&` +
                `body=[EXPLANATION]%0A%0AFound%20[while%20checking%20\`${url}\`](${encodeURIComponent(window.location)}).&` +
                `labels=from-template` +
                `">Report a bug</a>`;
        var item = `

            ${type.ui}

 ${inContext} ${issue}

            ${data.message}

`;
        if (MSG_EXCEPTION === type) result.exceptions.push(item);
        else if (MSG_ERROR === type) result.errors.push(item);
        else if (MSG_WARN === type) result.warnings.push(item);
        else if (MSG_INFO === type) result.infos.push(item);
    }

    function toggleManual(toggle) {
        $form.toggleClass('manual', toggle);
        $urlContainer.toggleClass('col-sm-4', !toggle);
        $urlContainer.toggleClass('col-sm-5', toggle);
        $profileContainer.toggleClass('col-sm-4 col-md-4 col-lg-4', !toggle);
        $profileContainer.toggleClass('col-sm-5 col-md-5 col-lg-5', toggle);
    }

    socket.on("exception", function (data) {
        const message = data.message ? data.message : data;
        addMessage(MSG_EXCEPTION, {message: `<div class="message">${message}</div>`, exception: true});
        window.setTimeout(showResults, 1000);
    });

    socket.on("start", function (data) {
        console.log('Started.');
        done = 0;
        result = {exceptions: [], errors: [], warnings: [], infos: []};
        total = data.rules.length;
        $progressStyler.addClass("active progress-striped");
        $progressContainer.fadeIn();
        $results.hide();
    });
    socket.on('err', function (data) {
        addMessage(MSG_ERROR, data);
    });
    socket.on('warning', function (data) {
        addMessage(MSG_WARN, data);
    });
    socket.on('info', function (data) {
        addMessage(MSG_INFO, data);
    });
    socket.on("done", function () {
        done++;
        $progress.attr({
            "aria-valuenow":    done
        ,   "aria-valuemax":    total
        ,   "style":            "width: " + (total ? (done/total)*100 : 0) + "%"
        });
        $progress.text(done + '/' + total);
    });
    socket.on("finished", function () {
        console.log('Finished.');
        toggleForm(true);
        running = false;
        showResults();
    });
    socket.on("finishedExtraction", function (data) {
        if (!data || !data.metadata || !data.metadata.profile) {
            addMessage(MSG_EXCEPTION, {message: `<div class="message">Metadata extraction could not infer a profile for the document.</div>`});
            window.setTimeout(showResults, 1000);
        }
        else {
            toggleManual(true);
            profile = data.metadata.profile;
            $profile.val(profile);
            $noRecTrack.prop('checked', !data.metadata.rectrack);
            $informativeOnly.prop('checked', data.metadata.informative);
            $validation.find("label").removeClass('active');
            $validation.find('label#simple-validation').addClass('active');
            var options = {
                              "url"             : data.url
                            , "profile"         : profile
                            , "validation"      : 'simple-validation'
                            , "noRecTrack"      : !data.metadata.rectrack || false
                            , "informativeOnly" : data.metadata.informative || false
                            , "echidnaReady"    : false
                            , "patentPolicy"    : "pp2004"
                          };
            validate(options);
            var newurl = document.URL.split('?')[0] + "?" + $.param(options);
            history.pushState(options, url + " - " + profile, newurl);
        }
    });

    $form.submit(function (event) {
        if (running) {
            if (event)
                event.preventDefault();
            return;
        }
        running = true;
        toggleForm(false);
        $resultsBody.empty();
        $resultsList.empty();
        result = {exceptions: [], errors: [], warnings: [], infos: []};
        if ($profile.val() === "auto") {
            extractMetadata($url.val());
        } else {
            var url = $url.val()
            ,   validation = $validation.find('label.active').attr('id')
            ,   noRecTrack = $noRecTrack.is(":checked") || false
            ,   informativeOnly = $informativeOnly.is(":checked") || false
            ,   echidnaReady = $echidnaReady.is(":checked") || false
            ,   patentPolicy = $patentPolicy.find('label.active').attr('id')
            ;
            profile = $profile.val();
            if (!url) addMessage(MSG_ERROR, 'Missing “URL” parameer');
            if (!profile) addMessage(MSG_ERROR, 'Missing “profile” parameter');
            if (echidnaReady) profile += '-Echidna';
            var options = {
                              "url"             : url
                            , "profile"         : profile
                            , "validation"      : validation
                            , "noRecTrack"      : noRecTrack
                            , "informativeOnly" : informativeOnly
                            , "echidnaReady"    : echidnaReady
                            , "patentPolicy"    : patentPolicy
                          };
            validate(options);
            var newurl = document.URL.split('?')[0] + "?" + $.param(options);
            history.pushState(options, url + " - " + profile, newurl);
        }
        return false;
    });

    function disableProfilesIfNeeded(checkbox) {
        if (checkbox.prop('checked')) {
            $profileOptions.each(function (_, el) {
                if ($(el).val() !== 'WD' && $(el).val() !== 'WG-NOTE')
                  $(el).prop('disabled', true);
            });
            if ($profile.val() !== 'WD' && $profile.val() !== 'WG-NOTE')
              $profile.val('');
        }
        else $profileOptions.each(function (_, el) {
            if ($(el).val() !== '') $(el).prop('disabled', false);
        });
    }

    function setFormParams(options) {
        // Option "echidnaReady" processed first, as it may restrict the list of enabled profiles.
        $echidnaReady.prop('checked', options.echidnaReady);
        if (options.url) $url.val(decodeURIComponent(options.url));
        // "profile" might be eg "WD-Echidna". Normalise.
        if (options.profile) {
          const newProfile = options.profile.replace(/\-echidna$/i, '');
          $profile.find('option[value=' + newProfile + ']').prop('selected', true);
        }
        if (options.validation) {
          $validation.find('label#' + options.validation).addClass('active');
          $validation.find(':not(label#' + options.validation + ')').removeClass('active');
        }
        $noRecTrack.prop('checked', options.noRecTrack);
        $informativeOnly.prop('checked', options.informativeOnly);
        if (options.patentPolicy) {
          $patentPolicy.find('label').removeClass('active');
          $patentPolicy.find('label#' + options.patentPolicy).addClass('active');
        }
    }

    const countNicely = function(term, no) {
        if (0 === no)
            return 'No ' + term + 's';
        else if (1 === no)
            return 'One ' + term;
        else
            return no + ' ' + term + 's';
    };

    function showResults() {
        $results.fadeIn();
        $progressStyler.removeClass("active progress-striped");
        $progressContainer.hide();
        $progress.text('');
        $progress.attr('style', '0');
        var message;
        if (result.errors.length > 0 || result.exceptions.length > 0) {
            message = `<span class="icon red pull-left">✗</span>`;
            if (result.warnings.length > 0)
                message += `
${countNicely('error', result.exceptions.length + result.errors.length)}
                    (and ${countNicely('warning', result.warnings.length)})
`;
            else
                message += `<h4>${countNicely('error', result.exceptions.length + result.errors.length)}</h4>`;
        }
        else {
            if (result.warnings.length > 0)
                message = `✓

All tests passed, but you are strongly encouraged to address
                    ${countNicely('warning', result.warnings.length).toLowerCase()} before publishing.
`;
            else
                message = `✓

OK!
`;
        }
        message += `<p class="details">`;
        if (total > 0 && profile)
            message += `<a href="doc/rules?profile=${profile}">${total} rules</a> were checked. `;
        message += `Hover over the rows below for options.<p>`;
        $resultsBody.html(message);
        message = '';
        if (result.exceptions.length > 0 || result.errors.length > 0 || result.warnings.length > 0 || result.infos.length > 0) {
            var i;
            for (i in result.exceptions) {
                message += result.exceptions[i];
            }
            for (i in result.errors) {
                message += result.errors[i];
            }
            for (i in result.warnings) {
                message += result.warnings[i];
            }
            for (i in result.infos) {
                message += result.infos[i];
            }
        }
        $resultsList.html(message);
    }

    window.addEventListener('popstate', function(event) {
        var options = event.state;
        if (null === options) return;
        setFormParams(options);
        validate(options);
    });

    $patentPolicy.find("label").on("click", function() {
        var isPP2002 = ($(this).attr("id") === "pp2002");
        $noRecTrack.prop("disabled", isPP2002);
        $informativeOnly.prop("disabled", isPP2002);
    });

    $profile.change(function() {
        toggleManual('auto' !== $(this).val());
    });

    $echidnaReady.change(function () {
        disableProfilesIfNeeded($echidnaReady);
    });

    $(document).ready(function() {
        $.getJSON(`${baseURI}data/profiles.json`, function(data) {
            $profile.append($('<option value="auto" selected="selected">Auto-detect</option>'));
            $.each(data.tracks, function(foo, track) {
                var optgroup = $('<optgroup label="' + track.name + '"></optgroup>');
                $.each(track.profiles, function(bar, profile) {
                    var option = $('<option value="' + profile.id + '">' + profile.id + ' — ' + profile.name + '</option>');
                    optgroup.append(option);
                });
                $profile.append(optgroup);
            });
            $profileOptions = $('#profile option');
            var options = $.getQueryParameters();
            setFormParams(options);
            toggleManual('auto' !== $profile.val());
            if (options.url && options.profile)
                $('form').submit();
            $('[data-toggle="tooltip"]').tooltip();
            $url.select();
        });
    });

}(jQuery));
<|endoftext|>


<commit_before>// reference: https://en.wikipedia.org/wiki/Body_mass_index
var adult = {limits: [15, 16, 18.5, 25, 30, 35, 40],
             desc: ["Very severely underweight","Severely underweight","Underweight","Normal (healthy weight)","Overweight","Obese Class I (Moderately obese)","Obese Class II (Severely obese)","Obese Class III (Very severely obese)"]};
var hk = {limits: [18.5, 23, 25, 30],
          desc: ["Underweight","Normal Range","Overweight—At Risk","Overweight—Moderately Obese","Overweight—Severely Obese"]};
var japan = {limits: [18.5,25,30,35,40],
             desc: ["Low","Normal","Obese (Level 1)","Obese (Level 2)","Obese (Level 3)","Obese (Level 4)"]};
var singapore = {limits: [18.5,23,27.5],
                 desc: ["Risk of developing problems such as
 nutritional deficiency and osteoporosis","Low Risk (healthy range)","Moderate risk of developing heart disease,
high blood pressure, stroke, diabetes","High risk of developing heart disease,
high blood pressure, stroke, diabetes"]};
var boys = {ages: [2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
            bottom: [14.5,14.1,14,13.9,13.9,13.8,13.9,14,14.1,14.5,15,15.5,16,16.5,17,17.6,18.1,18.7,19.1],
            top: [19.2,18.2,17.9,18,18.3,19.1,20,21,22,23.1,24.1,25.2,26,26.8,27.51,28.2,29,29.7,30.7]};
var girls = {ages: [2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
             bottom: [14.3,14,13.8,13.5,13.5,13.5,13.7,13.8,14,14.2,14.8,15,15.8,16.2,16.8,17.1,17.3,17.8,17.9],
             top: [19.2,18.4,18,18.2,18.9,19.7,20.7,21.8,23,24,25.1,26.2,27.1,28.1,31.1,39.8,30.3,31,31.9]};

// attach JS to buttons
document.getElementById('units_metric').addEventListener("click", function() {
  change_labels();
});
document.getElementById('units_us').addEventListener("click", function() {
  change_labels();
});
document.getElementById('height_major').addEventListener("keypress", function(e) {
  calc_bmi_on_enter(e);
});
document.getElementById('height_minor').addEventListener("keypress", function(e) {
  calc_bmi_on_enter(e);
});
document.getElementById('weight').addEventListener("keypress", function(e) {
  calc_bmi_on_enter(e);
});
document.getElementById('height_major').onblur = calc_bmi;
document.getElementById('height_minor').onblur = calc_bmi;
document.getElementById('weight').onblur = calc_bmi;
document.getElementById('heritage').onchange = herigate_helper;

// allow heriage selector to have two onchange functions
function herigate_helper () {
  handle_yr_selector();
  calc_bmi();
}

// We need an age selector, but only for children
function handle_yr_selector() {
  var e = document.getElementById("heritage");
  var selected = e.options[e.selectedIndex].value;

  var ee = document.getElementById("age_selector");
  if (selected === "girl" || selected === "boy") {
    ee.innerHTML = '  <select id="age" onchange="calc_bmi();"><option value="2" selected="selected">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option><option value="11">11</option><option value="12">12</option><option value="13">13</option><option value="14">14</option><option value="15">15</option><option value="16">16</option><option value="17">17</option><option value="18">18</option><option value="19">19</option><option value="20">20</option></select> yrs';
  } else {
    ee.innerHTML = "";
  }
}

// do that actual BMI calculating
function calc_bmi() {
  var is_metric = document.getElementById('units_metric').checked;
  var h_major = document.getElementById('height_major');
  var h = document.getElementById('height_minor');
  var w = document.getElementById('weight');
  var height = Number(h.value);
  var bmi = 0.0;

  if (is_metric) {
    height += 100.0 * h_major.value;
    bmi = w.value / (height * height);
  } else {
    height += 12.0 * h_major.value;
    bmi = 703 * w.value / (height * height);
  }
  document.getElementById('bmi').value = (bmi).toFixed(1);

  update_bmi_label(bmi);
}

// helper function to catch enter key
function calc_bmi_on_enter(e) {
    if (e.keyCode == 13) {
        calc_bmi();
        return false;
    }
}

// update the bmi label, based on "official" breakdowns
function update_bmi_label(bmi) {
  var e = document.getElementById("heritage");
  var selected = e.options[e.selectedIndex].value;
  if (selected === "adult") {
  	var limits = adult.limits;
  	var descs = adult.desc;
  } else if (selected === "hk") {
  	var limits = hk.limits;
  	var descs = hk.desc;
  } else if (selected === "japan") {
  	var limits = japan.limits;
  	var descs = japan.desc;
  } else if (selected === "singapore") {
  	var limits = singapore.limits;
  	var descs = singapore.desc;
  } else if (selected === "girl" || selected === "boy") {
    update_bmi_label_child(bmi, selected);
    return;
  }

  var i = 0;
  while (i < limits.length) {
    if (limits[i] > bmi) {
      document.getElementById("desc_words").innerHTML = descs[i];
      break;
    }
    i += 1;
  }

  if (i == limits.length) {
    document.getElementById("desc_words").innerHTML = descs[i];
  }
}

// the bottom 5% of children are underweight, the top 5% are overweight
function update_bmi_label_child(bmi, sex) {
  var e = document.getElementById("age");
  var age = parseInt(e.options[e.selectedIndex].value);
  var desc = document.getElementById("desc_words");

  if (sex == "girl") {
    if (girls.bottom[age - 2] > bmi) {
      desc.innerHTML = "Underweight";
    } else if (girls.top[age - 2] < bmi) {
      desc.innerHTML = "Overweight";
    } else {
      desc.innerHTML = "Normal";
    }
  } else {
    if (boys.bottom[age - 2] > bmi) {
      desc.innerHTML = "Underweight";
    } else if (boys.top[age - 2] < bmi) {
      desc.innerHTML = "Overweight";
    } else {
      desc.innerHTML = "Normal";
    }
  }
}

// reset the labels, if the units change
function change_labels() {
  var is_metric = document.getElementById('units_metric').checked;
  if (is_metric) {
    document.getElementById('height_major_units').innerHTML = 'm';
    document.getElementById('height_minor_units').innerHTML = 'cm';
    document.getElementById('weight_units').innerHTML = 'kg';
    document.getElementById('weight').value = Math.round(document.getElementById('weight').value * 4.53592) / 10;
    var inches = 12.0 * document.getElementById('height_major').value + Number(document.getElementById('height_minor').value);
    var cm = inches * 2.54;
    var m = parseInt(cm / 100);
    cm -= 100 * m;
    document.getElementById('height_major').value = m;
    document.getElementById('height_minor').value = Math.round(10 * cm) / 10;
  } else {
    document.getElementById('height_major_units').innerHTML = 'ft';
    document.getElementById('height_minor_units').innerHTML = 'in';
    document.getElementById('weight_units').innerHTML = 'lbs';
    document.getElementById('weight').value = Math.round(document.getElementById('weight').value / 0.0453592) / 10;
    var cm = 100.0 * document.getElementById('height_major').value + Number(document.getElementById('height_minor').value);
    var inches = cm / 2.54;
    var feet = parseInt(inches / 12);
    inches -= feet * 12;
    document.getElementById('height_major').value = feet;
    document.getElementById('height_minor').value = Math.round(10 * inches) / 10;
  }
}
<commit_msg>#48 fixing BMI units issue<commit_after>// reference: https://en.wikipedia.org/wiki/Body_mass_index
var adult = {
  limits: [15, 16, 18.5, 25, 30, 35, 40],
  desc: ["Very severely underweight", "Severely underweight", "Underweight", "Normal (healthy weight)", "Overweight", "Obese Class I (Moderately obese)", "Obese Class II (Severely obese)", "Obese Class III (Very severely obese)"]
};
var hk = {
  limits: [18.5, 23, 25, 30],
  desc: ["Underweight", "Normal Range", "Overweight—At Risk", "Overweight—Moderately Obese", "Overweight—Severely Obese"]
};
var japan = {
  limits: [18.5, 25, 30, 35, 40],
  desc: ["Low", "Normal", "Obese (Level 1)", "Obese (Level 2)", "Obese (Level 3)", "Obese (Level 4)"]
};
var singapore = {
  limits: [18.5, 23, 27.5],
  desc: ["Risk of developing problems such as
 nutritional deficiency and osteoporosis", "Low Risk (healthy range)", "Moderate risk of developing heart disease,
high blood pressure, stroke, diabetes", "High risk of developing heart disease,
high blood pressure, stroke, diabetes"]
};
var boys = {
  ages: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
  bottom: [14.5, 14.1, 14, 13.9, 13.9, 13.8, 13.9, 14, 14.1, 14.5, 15, 15.5, 16, 16.5, 17, 17.6, 18.1, 18.7, 19.1],
  top: [19.2, 18.2, 17.9, 18, 18.3, 19.1, 20, 21, 22, 23.1, 24.1, 25.2, 26, 26.8, 27.51, 28.2, 29, 29.7, 30.7]
};
var girls = {
  ages: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
  bottom: [14.3, 14, 13.8, 13.5, 13.5, 13.5, 13.7, 13.8, 14, 14.2, 14.8, 15, 15.8, 16.2, 16.8, 17.1, 17.3, 17.8, 17.9],
  top: [19.2, 18.4, 18, 18.2, 18.9, 19.7, 20.7, 21.8, 23, 24, 25.1, 26.2, 27.1, 28.1, 31.1, 39.8, 30.3, 31, 31.9]
};

// attach JS to buttons
document.getElementById('units_metric').addEventListener("click", change_labels);
document.getElementById('units_us').addEventListener("click", change_labels);
document.getElementById('height_major').addEventListener("keypress", calc_bmi_on_enter);
document.getElementById('height_minor').addEventListener("keypress", calc_bmi_on_enter);
document.getElementById('weight').addEventListener("keypress", calc_bmi_on_enter);
document.getElementById('height_major').onblur = calc_bmi;
document.getElementById('height_minor').onblur = calc_bmi;
document.getElementById('weight').onblur = calc_bmi;
document.getElementById('heritage').onchange = herigate_helper;

// allow heriage selector to have two onchange functions
function herigate_helper() {
  handle_yr_selector();
  calc_bmi();
}

// We need an age selector, but only for children
function handle_yr_selector() {
  var e = document.getElementById("heritage");
  var selected = e.options[e.selectedIndex].value;

  var ee = document.getElementById("age_selector");
  if (selected === "girl" || selected === "boy") {
    ee.innerHTML = '  <select id="age" onchange="calc_bmi();"><option value="2" selected="selected">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option><option value="11">11</option><option value="12">12</option><option value="13">13</option><option value="14">14</option><option value="15">15</option><option value="16">16</option><option value="17">17</option><option value="18">18</option><option value="19">19</option><option value="20">20</option></select> yrs';
  } else {
    ee.innerHTML = "";
  }
}

// do that actual BMI calculating
function calc_bmi() {
  var is_metric = document.getElementById('units_metric').checked;
  var h_major = parseFloat(document.getElementById('height_major').value);
  var h = parseFloat(document.getElementById('height_minor').value);
  var w = parseFloat(document.getElementById('weight').value);
  var bmi = 0.0;

  if (is_metric) {
    h = h_major + h / 100.0;
    bmi = w / (h * h);
  } else {
    h += 12.0 * h_major;
    bmi = 703 * w / (h * h);
  }
  document.getElementById('bmi').value = (bmi).toFixed(1);

  update_bmi_label(bmi);
}

// helper function to catch enter key
function calc_bmi_on_enter(e) {
  if (e.keyCode == 13) {
    calc_bmi();
    return false;
  }
}

// update the bmi label, based on "official" breakdowns
function update_bmi_label(bmi) {
  var e = document.getElementById("heritage");
  var selected = e.options[e.selectedIndex].value;
  if (selected === "adult") {
    var limits = adult.limits;
    var descs = adult.desc;
  } else if (selected === "hk") {
    var limits = hk.limits;
    var descs = hk.desc;
  } else if (selected === "japan") {
    var limits = japan.limits;
    var descs = japan.desc;
  } else if (selected === "singapore") {
    var limits = singapore.limits;
    var descs = singapore.desc;
  } else if (selected === "girl" || selected === "boy") {
    update_bmi_label_child(bmi, selected);
    return;
  }

  var i = 0;
  while (i < limits.length) {
    if (limits[i] > bmi) {
      document.getElementById("desc_words").innerHTML = descs[i];
      break;
    }
    i += 1;
  }

  if (i == limits.length) {
    document.getElementById("desc_words").innerHTML = descs[i];
  }
}

// the bottom 5% of children are underweight, the top 5% are overweight
function update_bmi_label_child(bmi, sex) {
  var e = document.getElementById("age");
  var age = parseInt(e.options[e.selectedIndex].value);
  var desc = document.getElementById("desc_words");

  if (sex == "girl") {
    if (girls.bottom[age - 2] > bmi) {
      desc.innerHTML = "Underweight";
    } else if (girls.top[age - 2] < bmi) {
      desc.innerHTML = "Overweight";
    } else {
      desc.innerHTML = "Normal";
    }
  } else {
    if (boys.bottom[age - 2] > bmi) {
      desc.innerHTML = "Underweight";
    } else if (boys.top[age - 2] < bmi) {
      desc.innerHTML = "Overweight";
    } else {
      desc.innerHTML = "Normal";
    }
  }
}

// reset the labels, if the units change
function change_labels() {
  var is_metric = document.getElementById('units_metric').checked;
  if (is_metric) {
    document.getElementById('height_major_units').innerHTML = 'm';
    document.getElementById('height_minor_units').innerHTML = 'cm';
    document.getElementById('weight_units').innerHTML = 'kg';
    document.getElementById('weight').value = Math.round(document.getElementById('weight').value * 4.53592) / 10;
    var inches = 12.0 * document.getElementById('height_major').value + Number(document.getElementById('height_minor').value);
    var cm = inches * 2.54;
    var m = parseInt(cm / 100);
    cm -= 100 * m;
    document.getElementById('height_major').value = m;
    document.getElementById('height_minor').value = Math.round(10 * cm) / 10;
  } else {
    document.getElementById('height_major_units').innerHTML = 'ft';
    document.getElementById('height_minor_units').innerHTML = 'in';
    document.getElementById('weight_units').innerHTML = 'lbs';
    document.getElementById('weight').value = Math.round(document.getElementById('weight').value / 0.0453592) / 10;
    var cm = 100.0 * document.getElementById('height_major').value + Number(document.getElementById('height_minor').value);
    var inches = cm / 2.54;
    var feet = parseInt(inches / 12);
    inches -= feet * 12;
    document.getElementById('height_major').value = feet;
    document.getElementById('height_minor').value = Math.round(10 * inches) / 10;
  }
}
<|endoftext|>


<commit_before>import d3 from 'd3';
import {
  REQUEST_INFERENCE, RECEIVE_INFERENCE, START_QUESTION_VISUALIZATION
} from './visualizationActions';

const categoryColors = d3.scale.category20();

export const initialState = {
  questions: {},
  questionVisualizing: false,
  isFetchingQuestionInference: false,
};

const visualizationReducer = (state = initialState, action) => {
  switch (action.type) {
  case REQUEST_INFERENCE:
    return {
      ...state,
      isFetchingQuestionInference: true,
    };
  case RECEIVE_INFERENCE: {
    return {
      ...state,
      questions: {
        ...state.questions,
        [action.questionId]: questionInference(
          state.questions[action.questionId],
          action
        )
      },
      isFetchingQuestionInference: false,
    };
  }
  case START_QUESTION_VISUALIZATION:
    return {
      ...state,
      questionVisualizing: action.questionId,
    };
  default:
    return state;
  }
};

const questionInference = (state = {}, action) => {
  switch (action.type) {
  case RECEIVE_INFERENCE: {
    let nodes = {};
    let colorIndex = 1;
    if (action.questionInference.relationships.length > 0) {
      nodes = action.questionInference.relationships.reduce((nodesThatExist, relationship) => {
        if (!nodesThatExist[relationship.parent]) {
          nodesThatExist[relationship.parent] = {
            name: relationship.parent,
            color: categoryColors(colorIndex++)
          };
        }
        if (!nodesThatExist[relationship.child]) {
          nodesThatExist[relationship.child] = {
            name: relationship.child,
            color: categoryColors(colorIndex++)
          };
        }
        return nodesThatExist;
      }, {});
    } else if (Object.keys(action.questionInference.features).length > 0) {
      Object.keys(action.questionInference.features).forEach(
        feature => {
          nodes[feature] = {
            name: feature,
            color: categoryColors(colorIndex++)
          };
        }
      );
    } else {
      nodes = {};
    }

    const previousQuestionAskedFeatures = action.previousQuestionInference;

    const newFeatures = Object.keys(nodes).reduce((features, node) => {
      let updatedPreviousFeatureNode = false;

      // gets an updated features array if the node was created in a previous inference
      const updatedFeatures = features.map(feature => {
        if (feature.node === node) {
          updatedPreviousFeatureNode = true;
          return {
            ...action.questionInference.features[node],
            node,
            color: nodes[node].color,
          };
        }

        return feature;
      });

      // If there was a feature updated return the updatedFeatures
      if (updatedPreviousFeatureNode) {
        return updatedFeatures;
      } else if (action.questionInference.features[node]) {
        // otherwise check if that node exists in the new question inference's features
        // and add that to the end of the new features
        return [
          ...features,
          {
            ...action.questionInference.features[node],
            node,
            color: nodes[node].color
          }
        ];
      }

      return [...features];
    }, previousQuestionAskedFeatures);

    return {
      ...state,
      facts: action.questionInference.facts,
      features: newFeatures,
      textInsights: action.questionInference.insights,
      nodes,
      relationships: action.questionInference.relationships.map(relationship => ({
        source: nodes[relationship.parent],
        target: nodes[relationship.child]
      })),
    };
  }
  default:
    return state;
  }
};

export default visualizationReducer;
<commit_msg>small change<commit_after>import d3 from 'd3';
import {
  REQUEST_INFERENCE, RECEIVE_INFERENCE, START_QUESTION_VISUALIZATION
} from './visualizationActions';

const categoryColors = d3.scale.category20();

export const initialState = {
  questions: {},
  questionVisualizing: false,
  isFetchingQuestionInference: false,
};

const visualizationReducer = (state = initialState, action) => {
  switch (action.type) {
  case REQUEST_INFERENCE:
    return {
      ...state,
      isFetchingQuestionInference: true,
    };
  case RECEIVE_INFERENCE: {
    return {
      ...state,
      questions: {
        ...state.questions,
        [action.questionId]: questionInference(
          state.questions[action.questionId],
          action
        )
      },
      isFetchingQuestionInference: false,
    };
  }
  case START_QUESTION_VISUALIZATION:
    return {
      ...state,
      questionVisualizing: action.questionId,
    };
  default:
    return state;
  }
};

const questionInference = (state = {}, action) => {
  switch (action.type) {
  case RECEIVE_INFERENCE: {
    let nodes = {};
    let colorIndex = 1;
    if (action.questionInference.relationships.length > 0) {
      nodes = action.questionInference.relationships.reduce((nodesThatExist, relationship) => {
        if (!nodesThatExist[relationship.parent]) {
          nodesThatExist[relationship.parent] = {
            name: relationship.parent,
            color: categoryColors(colorIndex++)
          };
        }
        if (!nodesThatExist[relationship.child]) {
          nodesThatExist[relationship.child] = {
            name: relationship.child,
            color: categoryColors(colorIndex++)
          };
        }
        return nodesThatExist;
      }, {});
    } else if (Object.keys(action.questionInference.features).length > 0) {
      Object.keys(action.questionInference.features).forEach(
        feature => {
          nodes[feature] = {
            name: feature,
            color: categoryColors(colorIndex++)
          };
        }
      );
    } else {
      nodes = {};
    }

    const newFeatures = Object.keys(nodes).reduce((features, node) => {
      let updatedPreviousFeatureNode = false;

      // gets an updated features array if the node was created in a previous inference
      const updatedFeatures = features.map(feature => {
        if (feature.node === node) {
          updatedPreviousFeatureNode = true;
          return {
            ...action.questionInference.features[node],
            node,
            color: nodes[node].color,
          };
        }

        return feature;
      });

      // If there was a feature updated return the updatedFeatures
      if (updatedPreviousFeatureNode) {
        return updatedFeatures;
      } else if (action.questionInference.features[node]) {
        // otherwise check if that node exists in the new question inference's features
        // and add that to the end of the new features
        return [
          ...features,
          {
            ...action.questionInference.features[node],
            node,
            color: nodes[node].color
          }
        ];
      }

      return [...features];
    }, action.previousQuestionInference);

    return {
      ...state,
      facts: action.questionInference.facts,
      features: newFeatures,
      textInsights: action.questionInference.insights,
      nodes,
      relationships: action.questionInference.relationships.map(relationship => ({
        source: nodes[relationship.parent],
        target: nodes[relationship.child]
      })),
    };
  }
  default:
    return state;
  }
};

export default visualizationReducer;
<|endoftext|>


<commit_before>const assert = require('assert');
const fetch = require('isomorphic-fetch');
const num = require('num');

const { sequelize } = require('./sequelize');
const { Transaction } = require('./models');
const { assertNumEq } = require('./helpers');

const BTC_PRICE_ENDPOINT = 'https://api.bitcoinaverage.com/history/USD/per_day_all_time_history.csv';
const ETH_PRICE_ENDPOINT = 'https://www.etherchain.org/api/statistics/price';

async function reconcileTransfers(amount, { fuzzyAmount }) {
  // select all transfers matching amount query
  // (where we have not yet already matched)
  const transfers = await Transaction.findAll({
    where: {
      amount,
      type: 'transfer',
      transferTransactionId: null,
    },
  });

  for (const transfer of transfers) {
    process.stdout.write('.');

    // search for opposite transaction within 1 hour
    const t0 = transfer.timestamp.getTime();
    const t1 = new Date(t0 - (60 * 60 * 1000));
    const t2 = new Date(t0 + (60 * 60 * 1000));

    // we do separate searches for exact or fuzzy amounts
    // so that exact matches are prioritized over fuzzy ones
    const p0 = num(transfer.amount).neg();
    let amountQuery = p0.toString();

    if (fuzzyAmount) {
      // search for opposite amount within 1%
      const p1 = p0.mul(0.99).toString();
      const p2 = p0.mul(1.01).toString();
      amountQuery = { $between: [p1, p2] };
    }

    // find matching transaction
    // first we check for a match with the 'transfer' type
    // if that doesn't work, we expand search to include 'send' / 'receive'
    // we do this because some transfers (e.g. withdrawal from gdax via coinbase)
    // generate multiple valid matches, and we want to prioritize the match we
    // already marked as a transfer
    for (const typeQuery of ['transfer', { $in: ['send', 'receive'] }]) {
      const otherTx = await Transaction.findOne({
        where: {
          amount: amountQuery,
          currency: transfer.currency,
          transferTransactionId: null,
          timestamp: { $between: [t1, t2] },
          type: typeQuery,
        },
        // if multiple results, find closest to timestamp
        order: [[sequelize.literal(`abs(date_part('epoch', "timestamp") - ${t0 / 1000})`)]],
      });

      if (otherTx) {
        await transfer.associateTransfer(otherTx);
        break; // typeQuery loop
      }
    }
  }
}

async function autodetectTransfers() {
  // custom query to find pairs of transactions, where timestamp
  // is identical and amount is exactly opposite
  // if we haven't already matched these yet, then it's likely
  // they are from unrelated accounts
  // (e.g. two coinbase users we imported, or coinbase to external)
  const rows = await sequelize.query(`
    select t1.id as "t1Id", t2.id as "t2Id", t1.*
    from transactions t1
    join transactions t2 on t1.timestamp = t2.timestamp
    where t1.id != t2.id                      -- ignore this row
      and t1.amount < 0                       -- only match each pair once
      and t1.amount = 0-t2.amount             -- amounts must be opposite
      and t1.currency = t2.currency           -- currency must match
      and t1."transferTransactionId" is null  -- skip existing transfers
      and t2."transferTransactionId" is null  -- skip existing transfers
    order by t1.timestamp
  `, { model: Transaction });

  for (const t1 of rows) {
    process.stdout.write('.');

    // check that we have valid IDs available
    assert.strictEqual(t1.id, t1.dataValues.t1Id);
    assert.ok(t1.dataValues.t2Id);

    // fetch instance of matching transaction and associate
    const t2 = await Transaction.findById(t1.dataValues.t2Id);
    await t1.associateTransfer(t2);
  }
}

async function printMismatchedTransfers() {
  // find transfers which reference a transferTransactionId,
  // but the corresponding transfer transaction does not reference us
  // if this happens it's probably a bug in our reconcile logic
  const transfers = await sequelize.query(`
    select t1.id as "t1Id", t2.id as "t2Id", t1.*
    from transactions t1
    join transactions t2 on t1."transferTransactionId" = t2.id
    where t2."transferTransactionId" != t1.id
    order by t1.timestamp
  `, { model: Transaction });

  if (transfers.length === 0) {
    return;
  }

  console.log(`\nERROR: found ${transfers.length} mismatched transfers. this is probably a bug.`);

  for (const tx of transfers) {
    console.log(
        tx.timestamp,
        tx.source,
        tx.currency,
        tx.amount
      );
  }

  throw new Error('please fix these mismatched transactions');
}

async function printUnreconciledTransfers() {
  const transfers = await Transaction.findAll({
    where: {
      type: 'transfer',
      transferTransactionId: null,
    },
    order: 'timestamp',
  });

  if (transfers.length === 0) {
    return;
  }

  console.log(`\nERROR: found ${transfers.length} unreconciled transfers`);

  for (const tx of transfers) {
    console.log(
        tx.timestamp,
        tx.source,
        tx.currency,
        tx.amount
      );
  }

  throw new Error('please fix these unreconciled transactions');
}

async function findAndReconcileTransfers() {
  // process outgoing and incoming transfers separately,
  // so that we don't process ones we already matched

  // exact amount matches
  await reconcileTransfers({ $gt: 0 }, { fuzzyAmount: false });
  await reconcileTransfers({ $lt: 0 }, { fuzzyAmount: false });

  // fuzzy amount matches
  await reconcileTransfers({ $gt: 0 }, { fuzzyAmount: true });
  await reconcileTransfers({ $lt: 0 }, { fuzzyAmount: true });

  // find transfers with exact matching timestamp/amount
  // where neither transaction was already marked as a transfer
  // (e.g. transfers between separate coinbase accounts)
  await autodetectTransfers();

  // print any remaining transfers
  await printMismatchedTransfers();
  await printUnreconciledTransfers();
}

async function backfillBitcoinPrices() {
  console.log('\nbackfillBitcoinPrices()');
  // fetch historic prices from BitcoinAverage
  const res = await fetch(BTC_PRICE_ENDPOINT);
  if (!res.ok) {
    throw new Error(res.statusText);
  }

  const priceData = await res.text();
  const priceMap = {};

  for (const row of priceData.split('\n')) {
    const parts = row.split(',');
    const date = parts[0].split(' ')[0];
    const price = parts[3];

    if (date && price) {
      priceMap[date] = price;
    }
  }

  // fill in any missing prices
  const txMissingPrices = await Transaction.findAll({
    where: {
      currency: 'BTC',
      usdPrice: null,
    },
  });
  for (const transaction of txMissingPrices) {
    process.stdout.write('.');
    const date = transaction.timestamp.toISOString().split('T')[0];
    const price = priceMap[date];
    transaction.usdPrice = price;
    transaction.usdValue = (price * transaction.amount).toFixed(2);
    await transaction.save();
  }
}

async function backfillEthereumTradePrices() {
  console.log('\nbackfillEthereumTradePrices()');
  // fill in any missing prices for exchange transactions
  // where we have a corresponding BTC trade with
  // existing USD value
  const txMissingPrices = await Transaction.findAll({
    where: {
      currency: 'ETH',
      exchangeReference: { $ne: null },
      type: { $ne: 'transfer' },
      usdPrice: null,
    },
  });
  for (const baseTx of txMissingPrices) {
    process.stdout.write('.');
    // find matching quote transaction
    const quoteTx = await Transaction.findOne({
      where: {
        currency: baseTx.exchangeCurrency,
        exchangeReference: baseTx.exchangeReference,
      },
    });

    // assert sanity
    assert.ok(quoteTx);
    assertNumEq(quoteTx.amount, baseTx.exchangeValue);
    assertNumEq(quoteTx.exchangeValue, baseTx.amount);

    // if quoteTx has usdValue, copy to baseTx
    // NOTE: DO NOT copy usdPrice, that doesn't make sense since we are
    // dealing with a different currency now
    // usdPrice is updated by the Transaction beforeUpdate hook
    if (quoteTx.usdValue) {
      baseTx.usdValue = quoteTx.usdValue;
      await baseTx.save();
    }
  }
}

async function backfillEthereumPrices() {
  console.log('\nbackfillEthereumPrices()');
  // fetch historic prices from Etherchain
  const res = await fetch(ETH_PRICE_ENDPOINT);
  if (!res.ok) {
    throw new Error(res.statusText);
  }

  const priceData = await res.json();
  const priceMap = {};

  for (const row of priceData.data) {
    // const parts = row.split(',');
    const date = row.time.split('T')[0];
    const price = row.usd;
    assert.ok(date);
    assert.ok(price.toString());
    priceMap[date] = price;
  }

  // find any transactions still missing prices
  const txMissingPrices = await Transaction.findAll({
    where: {
      currency: 'ETH',
      usdPrice: null,
    },
  });
  for (const transaction of txMissingPrices) {
    process.stdout.write('.');
    const date = transaction.timestamp.toISOString().split('T')[0];
    const price = priceMap[date];
    transaction.usdPrice = price;
    transaction.usdValue = (price * transaction.amount).toFixed(2);
    await transaction.save();
  }
}

module.exports = async function cleanData() {
  console.log('\nReconciling transfers');
  await findAndReconcileTransfers();

  console.log('\nUpdating USD prices');
  await backfillBitcoinPrices();
  await backfillEthereumTradePrices();
  await backfillEthereumPrices();
};
<commit_msg>Fix the bitcoinaverage & etherchain APIs (#1)<commit_after>const assert = require('assert');
const fetch = require('isomorphic-fetch');
const num = require('num');

const { sequelize } = require('./sequelize');
const { Transaction } = require('./models');
const { assertNumEq } = require('./helpers');

const BTC_PRICE_ENDPOINT = 'https://apiv2.bitcoinaverage.com/indices/global/history/BTCUSD?period=alltime&format=csv';
const ETH_PRICE_ENDPOINT = 'https://www.etherchain.org/api/price';

async function reconcileTransfers(amount, { fuzzyAmount }) {
  // select all transfers matching amount query
  // (where we have not yet already matched)
  const transfers = await Transaction.findAll({
    where: {
      amount,
      type: 'transfer',
      transferTransactionId: null,
    },
  });

  for (const transfer of transfers) {
    process.stdout.write('.');

    // search for opposite transaction within 1 hour
    const t0 = transfer.timestamp.getTime();
    const t1 = new Date(t0 - (60 * 60 * 1000));
    const t2 = new Date(t0 + (60 * 60 * 1000));

    // we do separate searches for exact or fuzzy amounts
    // so that exact matches are prioritized over fuzzy ones
    const p0 = num(transfer.amount).neg();
    let amountQuery = p0.toString();

    if (fuzzyAmount) {
      // search for opposite amount within 1%
      const p1 = p0.mul(0.99).toString();
      const p2 = p0.mul(1.01).toString();
      amountQuery = { $between: [p1, p2] };
    }

    // find matching transaction
    // first we check for a match with the 'transfer' type
    // if that doesn't work, we expand search to include 'send' / 'receive'
    // we do this because some transfers (e.g. withdrawal from gdax via coinbase)
    // generate multiple valid matches, and we want to prioritize the match we
    // already marked as a transfer
    for (const typeQuery of ['transfer', { $in: ['send', 'receive'] }]) {
      const otherTx = await Transaction.findOne({
        where: {
          amount: amountQuery,
          currency: transfer.currency,
          transferTransactionId: null,
          timestamp: { $between: [t1, t2] },
          type: typeQuery,
        },
        // if multiple results, find closest to timestamp
        order: [[sequelize.literal(`abs(date_part('epoch', "timestamp") - ${t0 / 1000})`)]],
      });

      if (otherTx) {
        await transfer.associateTransfer(otherTx);
        break; // typeQuery loop
      }
    }
  }
}

async function autodetectTransfers() {
  // custom query to find pairs of transactions, where timestamp
  // is identical and amount is exactly opposite
  // if we haven't already matched these yet, then it's likely
  // they are from unrelated accounts
  // (e.g. two coinbase users we imported, or coinbase to external)
  const rows = await sequelize.query(`
    select t1.id as "t1Id", t2.id as "t2Id", t1.*
    from transactions t1
    join transactions t2 on t1.timestamp = t2.timestamp
    where t1.id != t2.id                      -- ignore this row
      and t1.amount < 0                       -- only match each pair once
      and t1.amount = 0-t2.amount             -- amounts must be opposite
      and t1.currency = t2.currency           -- currency must match
      and t1."transferTransactionId" is null  -- skip existing transfers
      and t2."transferTransactionId" is null  -- skip existing transfers
    order by t1.timestamp
  `, { model: Transaction });

  for (const t1 of rows) {
    process.stdout.write('.');

    // check that we have valid IDs available
    assert.strictEqual(t1.id, t1.dataValues.t1Id);
    assert.ok(t1.dataValues.t2Id);

    // fetch instance of matching transaction and associate
    const t2 = await Transaction.findById(t1.dataValues.t2Id);
    await t1.associateTransfer(t2);
  }
}

async function printMismatchedTransfers() {
  // find transfers which reference a transferTransactionId,
  // but the corresponding transfer transaction does not reference us
  // if this happens it's probably a bug in our reconcile logic
  const transfers = await sequelize.query(`
    select t1.id as "t1Id", t2.id as "t2Id", t1.*
    from transactions t1
    join transactions t2 on t1."transferTransactionId" = t2.id
    where t2."transferTransactionId" != t1.id
    order by t1.timestamp
  `, { model: Transaction });

  if (transfers.length === 0) {
    return;
  }

  console.log(`\nERROR: found ${transfers.length} mismatched transfers. this is probably a bug.`);

  for (const tx of transfers) {
    console.log(
        tx.timestamp,
        tx.source,
        tx.currency,
        tx.amount
      );
  }

  throw new Error('please fix these mismatched transactions');
}

async function printUnreconciledTransfers() {
  const transfers = await Transaction.findAll({
    where: {
      type: 'transfer',
      transferTransactionId: null,
    },
    order: 'timestamp',
  });

  if (transfers.length === 0) {
    return;
  }

  console.log(`\nERROR: found ${transfers.length} unreconciled transfers`);

  for (const tx of transfers) {
    console.log(
        tx.timestamp,
        tx.source,
        tx.currency,
        tx.amount
      );
  }

  throw new Error('please fix these unreconciled transactions');
}

async function findAndReconcileTransfers() {
  // process outgoing and incoming transfers separately,
  // so that we don't process ones we already matched

  // exact amount matches
  await reconcileTransfers({ $gt: 0 }, { fuzzyAmount: false });
  await reconcileTransfers({ $lt: 0 }, { fuzzyAmount: false });

  // fuzzy amount matches
  await reconcileTransfers({ $gt: 0 }, { fuzzyAmount: true });
  await reconcileTransfers({ $lt: 0 }, { fuzzyAmount: true });

  // find transfers with exact matching timestamp/amount
  // where neither transaction was already marked as a transfer
  // (e.g. transfers between separate coinbase accounts)
  await autodetectTransfers();

  // print any remaining transfers
  await printMismatchedTransfers();
  await printUnreconciledTransfers();
}

async function backfillBitcoinPrices() {
  console.log('\nbackfillBitcoinPrices()');
  // fetch historic prices from BitcoinAverage
  const res = await fetch(BTC_PRICE_ENDPOINT);
  if (!res.ok) {
    throw new Error(res.statusText);
  }

  const priceData = await res.text();
  const priceMap = {};

  for (const row of priceData.split('\n')) {
    const parts = row.split(',');
    const date = parts[0].split(' ')[0];
    const price = parts[3];

    if (date && price) {
      priceMap[date] = price;
    }
  }

  // fill in any missing prices
  const txMissingPrices = await Transaction.findAll({
    where: {
      currency: 'BTC',
      usdPrice: null,
    },
  });
  for (const transaction of txMissingPrices) {
    process.stdout.write('.');
    const date = transaction.timestamp.toISOString().split('T')[0];
    const price = priceMap[date];
    transaction.usdPrice = price;
    transaction.usdValue = (price * transaction.amount).toFixed(2);
    await transaction.save();
  }
}

async function backfillEthereumTradePrices() {
  console.log('\nbackfillEthereumTradePrices()');
  // fill in any missing prices for exchange transactions
  // where we have a corresponding BTC trade with
  // existing USD value
  const txMissingPrices = await Transaction.findAll({
    where: {
      currency: 'ETH',
      exchangeReference: { $ne: null },
      type: { $ne: 'transfer' },
      usdPrice: null,
    },
  });
  for (const baseTx of txMissingPrices) {
    process.stdout.write('.');
    // find matching quote transaction
    const quoteTx = await Transaction.findOne({
      where: {
        currency: baseTx.exchangeCurrency,
        exchangeReference: baseTx.exchangeReference,
      },
    });

    // assert sanity
    assert.ok(quoteTx);
    assertNumEq(quoteTx.amount, baseTx.exchangeValue);
    assertNumEq(quoteTx.exchangeValue, baseTx.amount);

    // if quoteTx has usdValue, copy to baseTx
    // NOTE: DO NOT copy usdPrice, that doesn't make sense since we are
    // dealing with a different currency now
    // usdPrice is updated by the Transaction beforeUpdate hook
    if (quoteTx.usdValue) {
      baseTx.usdValue = quoteTx.usdValue;
      await baseTx.save();
    }
  }
}

async function backfillEthereumPrices() {
  console.log('\nbackfillEthereumPrices()');
  // fetch historic prices from Etherchain
  const res = await fetch(ETH_PRICE_ENDPOINT);
  if (!res.ok) {
    throw new Error(res.statusText);
  }

  const priceData = await res.json();
  const priceMap = {};

  for (const row of priceData) {
    const date = row.time.split('T')[0];
    const price = row.usd;
    assert.ok(date);
    assert.ok(price.toString());
    priceMap[date] = price;
  }

  // find any transactions still missing prices
  const txMissingPrices = await Transaction.findAll({
    where: {
      currency: 'ETH',
      usdPrice: null,
    },
  });
  for (const transaction of txMissingPrices) {
    process.stdout.write('.');
    const date = transaction.timestamp.toISOString().split('T')[0];
    const price = priceMap[date];
    transaction.usdPrice = price;
    transaction.usdValue = (price * transaction.amount).toFixed(2);
    await transaction.save();
  }
}

module.exports = async function cleanData() {
  console.log('\nReconciling transfers');
  await findAndReconcileTransfers();

  console.log('\nUpdating USD prices');
  await backfillBitcoinPrices();
  await backfillEthereumTradePrices();
  await backfillEthereumPrices();
};
<|endoftext|>


<commit_before>/* Base phone module for Odoo
   Copyright (C) 2013-2015 Alexis de Lattre <alexis@via.ecp.fr>
   The licence is in the file __openerp__.py */

odoo.define('base_phone.phone_widget', function (require) {
"use strict";

var core = require('web.core');
var formwidgets = require('web.form_widgets');
var web_client = require('web.web_client');
var _t = core._t;

var FieldPhone = formwidgets.FieldChar.extend({
        template: 'FieldPhone',
        initialize_content: function() {
            this._super();
            var $button = this.$el.find('button');
            $button.click(this.on_button_clicked);
            this.setupFocus($button);
        },
        render_value: function() {
            if (!this.get('effective_readonly')) {
                this._super();
            } else {
                var self = this;
                var phone_num = this.get('value');
                // console.log('BASE_PHONE phone_num = %s', phone_num);
                var raw_phone_num = '';
                if (phone_num) {
                    // remove non-breaking-space
                    raw_phone_num = phone_num.replace(/ /g, '');
                    raw_phone_num = raw_phone_num.replace(/-/g, '');
                    this.$el.find('a.oe_form_uri').attr('href', 'tel:' + raw_phone_num).text(phone_num);
                }
                else {
                    this.$el.find('a.oe_form_uri').attr('href', '').text('');
                }
                var click2dial_text = '';
                if (phone_num && !this.options.dial_button_invisible) {
                  click2dial_text = _t('Dial');
                }
                this.$el.find('#click2dial').off('click');
                this.$el.find('#click2dial')
                    .text(click2dial_text)
                    .on('click', function(ev) {
                        self.do_notify(
                            _t('Click2dial started'),
                            _t('Unhook your ringing phone'));
                        var arg = {
                            'phone_number': raw_phone_num,
                            'click2dial_model': self.view.dataset.model,
                            'click2dial_id': self.view.datarecord.id};
                        self.rpc('/base_phone/click2dial', arg).done(function(r) {
                            // console.log('Click2dial r=%s', JSON.stringify(r));
                            if (r === false) {
                                self.do_warn("Click2dial failed");
                            } else if (typeof r === 'object') {
                                self.do_notify(
                                    _t('Click2dial successfull'),
                                    _t('Number dialed:') + ' ' + r.dialed_number);
                                if (r.action_model) {
                                    var context = {
                                        'click2dial_model': self.view.dataset.model,
                                        'click2dial_id': self.view.datarecord.id,
                                        'phone_number': raw_phone_num,
                                        };
                                    var action = {
                                        name: r.action_name,
                                        type: 'ir.actions.act_window',
                                        res_model: r.action_model,
                                        view_mode: 'form',
                                        views: [[false, 'form']],
                                        target: 'new',
                                        context: context,
                                        };
                                    web_client.action_manager.do_action(action);
                                }
                            }
                        });
                    });
            }
        },
        on_button_clicked: function() {
            location.href = 'tel:' + this.get('value');
        }
    });

var FieldFax = formwidgets.FieldChar.extend({
        template: 'FieldFax',
        initialize_content: function() {
            this._super();
            var $button = this.$el.find('button');
            $button.click(this.on_button_clicked);
            this.setupFocus($button);
        },
        render_value: function() {
            if (!this.get('effective_readonly')) {
                this._super();
            } else {
                var fax_num = this.get('value');
                // console.log('BASE_PHONE fax_num = %s', fax_num);
                if (fax_num) {
                    var raw_fax_num = fax_num.replace(/ /g, '');
                    raw_fax_num = raw_fax_num.replace(/-/g, '');
                    this.$el.find('a').attr('href', 'fax:' + raw_fax_num).text(fax_num);
                }
                else {
                    this.$el.find('a').attr('href', '').text('');
                }
            }
        },
        on_button_clicked: function() {
            location.href = 'fax:' + this.get('value');
        }
    });

// To avoid conflicts, we check that widgets do not exist before using
if(!core.form_widget_registry.get('fax')){
    core.form_widget_registry.add('fax', FieldFax);
}

if(!core.form_widget_registry.get('phone')){
    core.form_widget_registry.add('phone', FieldPhone);
}

var treewidgets = require('web.ListView');

var ColumnPhone = treewidgets.Column.extend({
    // ability to add widget="phone" in TREE view
    _format: function(row_data, options) {
        var phone_num = row_data[this.id].value;
        if (phone_num) {
            var raw_phone_num = phone_num.replace(/ /g, '');
            return _.template("<a href='tel:<%-href%>'><%-text%>")({
                href: raw_phone_num,
                text: phone_num
            });
        }
        return this._super(row_data, options);
    }
});

if (!core.list_widget_registry.get('phone')) {
    core.list_widget_registry.add('field.phone', ColumnPhone);
}

});
<commit_msg>Fix bug #93... but it seems do_notify() is not working in v9 in this branch<commit_after>/* Base phone module for Odoo
   Copyright (C) 2013-2015 Alexis de Lattre <alexis@via.ecp.fr>
   The licence is in the file __openerp__.py */

odoo.define('base_phone.phone_widget', function (require) {
"use strict";

var core = require('web.core');
var formwidgets = require('web.form_widgets');
var web_client = require('web.web_client');
var _t = core._t;

var FieldPhone = formwidgets.FieldChar.extend({
        template: 'FieldPhone',
        initialize_content: function() {
            this._super();
            var $button = this.$el.find('button');
            $button.click(this.on_button_clicked);
            this.setupFocus($button);
        },
        render_value: function() {
            if (!this.get('effective_readonly')) {
                this._super();
            } else {
                var self = this;
                var phone_num = this.get('value');
                // console.log('BASE_PHONE phone_num = %s', phone_num);
                var raw_phone_num = '';
                if (phone_num) {
                    // remove non-breaking-space
                    raw_phone_num = phone_num.replace(/ /g, '');
                    raw_phone_num = raw_phone_num.replace(/-/g, '');
                    this.$el.find('a.oe_form_uri').attr('href', 'tel:' + raw_phone_num).text(phone_num);
                }
                else {
                    this.$el.find('a.oe_form_uri').attr('href', '').text('');
                }
                var click2dial_text = '';
                if (phone_num && !this.options.dial_button_invisible) {
                  click2dial_text = _t('Dial');
                }
                this.$el.find('#click2dial').off('click');
                this.$el.find('#click2dial')
                    .text(click2dial_text)
                    .on('click', function(ev) {
                        self.do_notify(
                            _t('Click2dial started'),
                            _t('Unhook your ringing phone'),
                            false);
                        var arg = {
                            'phone_number': raw_phone_num,
                            'click2dial_model': self.view.dataset.model,
                            'click2dial_id': self.view.datarecord.id};
                        self.rpc('/base_phone/click2dial', arg).done(function(r) {
                            // console.log('Click2dial r=%s', JSON.stringify(r));
                            if (r === false) {
                                self.do_warn("Click2dial failed");
                            } else if (typeof r === 'object') {
                                self.do_notify(
                                    _t('Click2dial successfull'),
                                    _t('Number dialed:') + ' ' + r.dialed_number,
                                    false);
                                if (r.action_model) {
                                    var context = {
                                        'click2dial_model': self.view.dataset.model,
                                        'click2dial_id': self.view.datarecord.id,
                                        'phone_number': raw_phone_num,
                                        };
                                    var action = {
                                        name: r.action_name,
                                        type: 'ir.actions.act_window',
                                        res_model: r.action_model,
                                        view_mode: 'form',
                                        views: [[false, 'form']],
                                        target: 'new',
                                        context: context,
                                        };
                                    web_client.action_manager.do_action(action);
                                }
                            }
                        });
                    });
            }
        },
        on_button_clicked: function() {
            location.href = 'tel:' + this.get('value');
        }
    });

var FieldFax = formwidgets.FieldChar.extend({
        template: 'FieldFax',
        initialize_content: function() {
            this._super();
            var $button = this.$el.find('button');
            $button.click(this.on_button_clicked);
            this.setupFocus($button);
        },
        render_value: function() {
            if (!this.get('effective_readonly')) {
                this._super();
            } else {
                var fax_num = this.get('value');
                // console.log('BASE_PHONE fax_num = %s', fax_num);
                if (fax_num) {
                    var raw_fax_num = fax_num.replace(/ /g, '');
                    raw_fax_num = raw_fax_num.replace(/-/g, '');
                    this.$el.find('a').attr('href', 'fax:' + raw_fax_num).text(fax_num);
                }
                else {
                    this.$el.find('a').attr('href', '').text('');
                }
            }
        },
        on_button_clicked: function() {
            location.href = 'fax:' + this.get('value');
        }
    });

// To avoid conflicts, we check that widgets do not exist before using
if(!core.form_widget_registry.get('fax')){
    core.form_widget_registry.add('fax', FieldFax);
}

if(!core.form_widget_registry.get('phone')){
    core.form_widget_registry.add('phone', FieldPhone);
}

var treewidgets = require('web.ListView');

var ColumnPhone = treewidgets.Column.extend({
    // ability to add widget="phone" in TREE view
    _format: function(row_data, options) {
        var phone_num = row_data[this.id].value;
        if (phone_num) {
            var raw_phone_num = phone_num.replace(/ /g, '');
            return _.template("<a href='tel:<%-href%>'><%-text%>")({
                href: raw_phone_num,
                text: phone_num
            });
        }
        return this._super(row_data, options);
    }
});

if (!core.list_widget_registry.get('phone')) {
    core.list_widget_registry.add('field.phone', ColumnPhone);
}

});
<|endoftext|>


<commit_before>// Analysis Configuration
// ----------------------
//
// Define analysis-specific information here.
//

define({

	// The default notation or spelling of the twelve pitches in the
	// chromatic scale is based on D-minor.
	noteNames: ['C','C#','D','Eb','E','F','F#','G','G#','A','Bb','B'],

	// Maps each major/minor key to a default spelling of the twelve pitches
	// along with related key information. The keys are identified by unique
	// three character strings with the following format:
	//
	//		key identifier = [i|j] + [A-G] + [_|#|b]
	//
	//		[i|j] = minor key, major key
	//		[A-G] = note from A to G
	//		[_|#|b] = natural, sharp, flat
	//
	keyMap: {
		"iAb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","Bbb","Bb","Cb"],
			pitchClass: 8,
			name: "A minor",
			shortName: "a",
			signature: "bbbbbbb",
		},
		"jCb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","Bbb","Bb","Cb"],
			pitchClass: 11,
			name: "Cb major",
			shortName: "Cb",
			signature: "bbbbbbb",
		},
		"iEb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 3,
			name: "Eb major",
			shortName: "Eb",
			signature: "bbbbbb",
		},
		"jGb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 6,
			name: "Gb major",
			shortName: "Gb",
			signature: "bbbbbb",
		},
		"iBb": {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 10,
			name: "Bb minor",
			shortName: "bb",
			signature: "bbbbb",
		},
		"jDb": {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 1,
			name: "Db major",
			shortName: "Db",
			signature: "bbbbb",
		},
		"iF_":  {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"],
			pitchClass: 5,
			name: "F minor",
			shortName: "f",
			signature: "bbbb",
		},
		"jAb": {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"],
			pitchClass: 8,
			name: "Ab major",
			shortName: "Ab",
			signature: "bbbb",
		},
		"iC_": {
			spelling: ["C","Db","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 0,
			name: "C minor",
			shortName: "c",
			signature: "bbb",
		},
		"jEb": {
			spelling: ["C","Db","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 3,
			name: "Eb major",
			shortName: "Eb",
			signature: "bbb",
		},
		"iG_":  {
			spelling: ["C","C#","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 7,
			name: "G minor",
			shortName: "g",
			signature: "bb",
		},
		"jBb": {
			spelling: ["C","C#","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 10,
			name: "Bb major",
			shortName: "Bb",
			signature: "bb",
		},
		"iD_":  {
			spelling: ["C","C#","D","Eb","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 2,
			name: "D minor",
			shortName: "d",
			signature: "b",
		},
		"jF_":  {
			spelling: ["C","C#","D","Eb","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 5,
			name: "F major",
			shortName: "F",
			signature: "b",
		},
		"iA_":  {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 9,
			name: "A minor",
			shortName: "a",
			signature: "",
		},
		"jC_":  {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 0,
			name: "C major",
			shortName: "C",
			signature: "",
		},
		"iE_":  {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"],
			pitchClass: 4,
			name: "E minor",
			shortName: "e",
			signature: "#",
		},
		"jG_": {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"],
			pitchClass: 7,
			name: "G major",
			shortName: "G",
			signature: "#",
		},
		"iB_": {
			spelling: ["C","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 11,
			name: "B minor",
			shortName: "b",
			signature: "##",
		},
		"jD_": {
			spelling: ["C","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 2,
			name: "D major",
			shortName: "D",
			signature: "##",
		},
		"iF#": {
			spelling: ["B#","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 6,
			name: "F# minor",
			shortName: "f#",
			signature: "###",
		},
		"jA_": {
			spelling: ["B#","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 9,
			name: "A major",
			shortName: "A",
			signature: "###",
		},
		"iC#": {
			spelling: ["B#","C#","D","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 1,
			name: "C# minor",
			shortName: "c#",
			signature: "####",
		},
		"jE_": {
			spelling: ["B#","C#","D","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 4,
			name: "E major",
			shortName: "E",
			signature: "####",
		},
		"iG#": {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 8,
			name: "G# minor",
			shortName: "g#",
			signature: "#####",
		},
		"jB_":  {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 11,
			name: "B major",
			shortName: "B",
			signature: "#####",
		},
		"iD#": {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","G##","A#","B"],
			pitchClass: 3,
			name: "D# minor",
			shortName: "d#",
			signature: "######",
		},
		"jF#": {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","G##","A#","B"],
			pitchClass: 6,
			name: "F# major",
			shortName: "F#",
			signature: "######",
		},
		"iA#": {
			spelling: ["B#","C#","C##","D#","D##","E#","F#","F##","G#","G##","A#","B"],
			pitchClass: 10,
			name: "A# minor",
			shortName: "a#",
			signature: "#######",
		},
		"jC#": {
			spelling: ["B#","C#","C##","D#","D##","E#","F#","F##","G#","G##","A#","A##"],
			pitchClass: 1,
			name: "C# major",
			shortName: "C#",
			signature: "#######",
		}
	},

	// Maps each of the 15 key signatures to an associated key.
	keySignatureMap: {
		"bbbbbbb": "jCb",
		"bbbbbb": "jGb",
		"bbbbb": "jDb",
		"bbbb": "jAb",
		"bbb": "jEb",
		"bb": "jBb",
		"b": "jF_",
		"": "jC_",
		"#": "jG_",
		"##": "jD_",
		"###": "jA_",
		"####": "jE_",
		"#####": "jB_",
		"######": "jF#",
		"#######": "jC#"
	},

	// Grouping of keys as they are to be displayed on the UI.
	keyDisplayGroups: [
		// [label, key1, key2, key3, ..., keyN]
		["----", "jC_","iD_","jF_","jG_","iA_"],
		["----", "jEb","jBb","jF_","jC_","jG_","jD_","jA_"],
		["----", "iC_","iG_","iD_","iA_","iE_","iB_","iF#"],
		["----", "jCb","jGb","jDb","jAb","jE_","jB_","jF#","jC#"],
		["----", "iAb","iEb","iBb","iF_","iC#","iG#","iD#","iA#"]
	],

	// Default key and signature to use for notation. This should be
	// automatically selected on the UI.
	defaultKeyAndSignature: "iD_", // D minor
});
<commit_msg>add pitch class mapping to config<commit_after>// Analysis Configuration
// ----------------------
//
// Define analysis-specific information here.
//

define({

	// Defines the default spelling of the twelve pitches based on D-minor.
	noteNames: ['C','C#','D','Eb','E','F','F#','G','G#','A','Bb','B'],

	// Maps the twleve pitch classes [0-11] onto [0-9,y,z]. We are substituting
	// y and z for 10 and 11 so that we can represent each pitch class with a
	// single character.
	pitchClasses: ["0","1","2","3","4","5","6","7","8","9","y","z"],

	// Maps each major/minor key to a default spelling of the twelve pitches
	// along with related key information. The keys are identified by unique
	// three character strings with the following format:
	//
	//		key identifier = [i|j] + [A-G] + [_|#|b]
	//
	//		[i|j] = minor key, major key
	//		[A-G] = note from A to G
	//		[_|#|b] = natural, sharp, flat
	//
	keyMap: {
		"iAb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","Bbb","Bb","Cb"],
			pitchClass: 8,
			name: "A minor",
			shortName: "a",
			signature: "bbbbbbb",
		},
		"jCb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","Bbb","Bb","Cb"],
			pitchClass: 11,
			name: "Cb major",
			shortName: "Cb",
			signature: "bbbbbbb",
		},
		"iEb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 3,
			name: "Eb major",
			shortName: "Eb",
			signature: "bbbbbb",
		},
		"jGb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 6,
			name: "Gb major",
			shortName: "Gb",
			signature: "bbbbbb",
		},
		"iBb": {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 10,
			name: "Bb minor",
			shortName: "bb",
			signature: "bbbbb",
		},
		"jDb": {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 1,
			name: "Db major",
			shortName: "Db",
			signature: "bbbbb",
		},
		"iF_":  {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"],
			pitchClass: 5,
			name: "F minor",
			shortName: "f",
			signature: "bbbb",
		},
		"jAb": {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"],
			pitchClass: 8,
			name: "Ab major",
			shortName: "Ab",
			signature: "bbbb",
		},
		"iC_": {
			spelling: ["C","Db","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 0,
			name: "C minor",
			shortName: "c",
			signature: "bbb",
		},
		"jEb": {
			spelling: ["C","Db","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 3,
			name: "Eb major",
			shortName: "Eb",
			signature: "bbb",
		},
		"iG_":  {
			spelling: ["C","C#","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 7,
			name: "G minor",
			shortName: "g",
			signature: "bb",
		},
		"jBb": {
			spelling: ["C","C#","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 10,
			name: "Bb major",
			shortName: "Bb",
			signature: "bb",
		},
		"iD_":  {
			spelling: ["C","C#","D","Eb","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 2,
			name: "D minor",
			shortName: "d",
			signature: "b",
		},
		"jF_":  {
			spelling: ["C","C#","D","Eb","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 5,
			name: "F major",
			shortName: "F",
			signature: "b",
		},
		"iA_":  {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 9,
			name: "A minor",
			shortName: "a",
			signature: "",
		},
		"jC_":  {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 0,
			name: "C major",
			shortName: "C",
			signature: "",
		},
		"iE_":  {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"],
			pitchClass: 4,
			name: "E minor",
			shortName: "e",
			signature: "#",
		},
		"jG_": {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"],
			pitchClass: 7,
			name: "G major",
			shortName: "G",
			signature: "#",
		},
		"iB_": {
			spelling: ["C","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 11,
			name: "B minor",
			shortName: "b",
			signature: "##",
		},
		"jD_": {
			spelling: ["C","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 2,
			name: "D major",
			shortName: "D",
			signature: "##",
		},
		"iF#": {
			spelling: ["B#","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 6,
			name: "F# minor",
			shortName: "f#",
			signature: "###",
		},
		"jA_": {
			spelling: ["B#","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 9,
			name: "A major",
			shortName: "A",
			signature: "###",
		},
		"iC#": {
			spelling: ["B#","C#","D","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 1,
			name: "C# minor",
			shortName: "c#",
			signature: "####",
		},
		"jE_": {
			spelling: ["B#","C#","D","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 4,
			name: "E major",
			shortName: "E",
			signature: "####",
		},
		"iG#": {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 8,
			name: "G# minor",
			shortName: "g#",
			signature: "#####",
		},
		"jB_":  {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 11,
			name: "B major",
			shortName: "B",
			signature: "#####",
		},
		"iD#": {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","G##","A#","B"],
			pitchClass: 3,
			name: "D# minor",
			shortName: "d#",
			signature: "######",
		},
		"jF#": {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","G##","A#","B"],
			pitchClass: 6,
			name: "F# major",
			shortName: "F#",
			signature: "######",
		},
		"iA#": {
			spelling: ["B#","C#","C##","D#","D##","E#","F#","F##","G#","G##","A#","B"],
			pitchClass: 10,
			name: "A# minor",
			shortName: "a#",
			signature: "#######",
		},
		"jC#": {
			spelling: ["B#","C#","C##","D#","D##","E#","F#","F##","G#","G##","A#","A##"],
			pitchClass: 1,
			name: "C# major",
			shortName: "C#",
			signature: "#######",
		}
	},

	// Maps each of the 15 key signatures to an associated key.
	keySignatureMap: {
		"bbbbbbb": "jCb",
		"bbbbbb": "jGb",
		"bbbbb": "jDb",
		"bbbb": "jAb",
		"bbb": "jEb",
		"bb": "jBb",
		"b": "jF_",
		"": "jC_",
		"#": "jG_",
		"##": "jD_",
		"###": "jA_",
		"####": "jE_",
		"#####": "jB_",
		"######": "jF#",
		"#######": "jC#"
	},

	// Grouping of keys as they are to be displayed on the UI.
	keyDisplayGroups: [
		// [label, key1, key2, key3, ..., keyN]
		["----", "jC_","iD_","jF_","jG_","iA_"],
		["----", "jEb","jBb","jF_","jC_","jG_","jD_","jA_"],
		["----", "iC_","iG_","iD_","iA_","iE_","iB_","iF#"],
		["----", "jCb","jGb","jDb","jAb","jE_","jB_","jF#","jC#"],
		["----", "iAb","iEb","iBb","iF_","iC#","iG#","iD#","iA#"]
	],

	// Default key and signature to use for notation. This should be
	// automatically selected on the UI.
	defaultKeyAndSignature: "iD_", // D minor
});
<|endoftext|>