<commit_before>// ==UserScript==
// @name         Extra Bitcointip Support on Reddit
// @description  Add a tipping button (/u/bitcointip) and shrinks verifications
// @version      1.3.4
// @license      Public Domain
// @include      http://*.reddit.com/*
// @exclude      http://*.reddit.com/user/bitcointip
// ==/UserScript==

/* Changelog:
 * 1.3.4
 *    Inject the bot status into r/bitcointip's sidebar
 * 1.3.3
 *    Fix API URL.
 *    Finer tip rejection control
 *    Small bug fixes
 * 1.3.1
 *    A tip button on posts
 *    Verification icons on posts
 *    Small display tweaks
 * 1.3.0
 *    New verification and rejection markings
 *    Use the new gettipped API
 *    All new jQuery reddit plugin
 * 1.2.1
 *    More efficient validation marking
 *    Fixed status names
 *    Run on all of reddit, including user pages
 * 1.2
 *    Use the new bitcointip API to get status information
 *    https://github.com/NerdfighterSean/bitcointip/blob/master/src/api/
 * 1.1.4
 *    Don't hide verification when it has children
 *    Don't hide verification when it's the URL target
 * 1.1.3
 *    Fix case insensitivity in regex
 * 1.1.2
 *    Display tipping amount
 * 1.1.1
 *    Support the full tipping syntax
 * 1.1
 *    Hide verification posts, inlining the verification
 * 1.0
 *    Add a "tip bitcoins" button.
 */

var baseTip = '0.01 BTC';
var tipregex = /((\+(bitcointip|bitcoin|tip|btctip|bittip|btc))|((\+((?!0)(\d{1,4})) internet(s)?)|(\+((?!0)(\d{1,4})) point(s)? to (Gryffindor|Slytherin|Ravenclaw|Hufflepuff))))/i;
var botDownThreshold = 15 * 60 * 1000; // milliseconds
var botStatusHtml = {
    up: 'UP',
    down: 'DOWN'
};
var api = {
    gettips: 'http://bitcointip.net/api/gettips.php?',
    gettipped: 'http://bitcointip.net/api/gettipped.php?'
};
var icons = {
    verified: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAMAAABFNRROAAAAt1BMVEX///8AAAAAyAAAuwAAwQcAvAcAvwAAwQYAyAUAxAUAxwQAwgQAvAMAxQYAvwYAxQYAxwU5yT060j460j871T89wUE9wkFGokdGu0hIzExJl09JmE9JxExJxE1K1U9K1k5Ll09LmVNMmVNM2FBNmlRRx1NSzlRTqlVUslZU1ldVq1hVrFdV2FhWrFhX21pZqlphrWJh3WRotGtrqm1stW91sXd2t3h5t3urz6zA2sHA28HG3sf4+PhvgZhQAAAAEXRSTlMAARweJSYoLTM0O0dMU1dYbkVIv+oAAACKSURBVHjaVc7XEoIwEIXhFRED1tBUxBaPFSyxK3n/5zIBb/yv9pudnVky2Ywxm345MHkVXByllPm4W24qrLbzdo1sLPPRepc+XlnSIAuz9DQYPtXnkLhUF/ysrndV3CYLRpbg2VtpxFMwfRfEl8IghEPUhB9t9lEQoke6FnzONfpU5kEIoKOn/z+/pREPWTic38sAAAAASUVORK5CYII=",
    rejected: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAMAAABFNRROAAAAQlBMVEX///+qAAAAAAC/AADIABSaTU3YMDDcPj7cSEjeUFDiZGTld3fmfHzoiorqkJDqlpbupKTuqqr99fX99/f+/Pz////kWqLlAAAABXRSTlMAAwcIM6KYVMQAAABfSURBVHjaXc7JDsAgCEVRsYpIBzro//9qHyHpond3AgkklIuXPKJcqleIIEB6FwEhQEW0t4rlUtt+22ZTMQ09NqZyiK8BtBCvc9iDWegY526hBVRmdcQ9RgD9f/G+P1+JEwRF2vKhRgAAAABJRU5ErkJggg==",
    tipped: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAANCAMAAACq939wAAAA/FBMVEWqVQCqcQCZZgCLXQCdYgDMjACOXACOXwCacQCfcQCqegCwggChggCnfwCwggCthACtgQC9iACleQC+iwDMlgDJlACmfgDDiwDBjADHlQDJnQDFlQDKmAChfRGjgBOmfhKmghKnghOqhBKthxOviROvjB+vjCGvjCOwiRSwihixixWxjSGziBOzkSmzky+0kSa0kiy3jxW8kxS8mCi8n0i8oEq9lBe9mSvOoBbUrTTUrTjbukXcsTDctDrdtj/exHbexXDfwWXfwmvjrBnksRjksx7lrhnqx17qyWTqymrq377rz3nr2qftuiHtvSv67cD67sj+997/+OX///8rcy1sAAAAHXRSTlMDCQoLDRQkKystMDc5Oj0+QUlKS0tMTVFSV15/i6wTI/gAAACWSURBVHjaHcrnAoFQGADQryI7sjNKN0RKZJVNQ8io938YN+f3ASDp1B9NAhD15UzXNH26KhNQXZyDBxZcNlloKadvFIbR56owUOFV9425ai8PrGwZaITQeisXoKHs7k/MP44ZaHYl54U5El8EdmjNEWbEjesf/Ljd9v0S1ETBtD3PNgUxB8nOQIybOGknAKgMy2FsmoIflIEZdK7PshkAAAAASUVORK5CYII="
};
var $ = unsafeWindow.$, reddit = unsafeWindow;

/* Add the "tip bitcoins" button after "give gold". */
var tip = $('tip bitcoins').attr({
    'class': 'tip-bitcoins login-required',
    'href': '#'
});
if (/^\/r\//.test(document.location.pathname)) {
    $('a.give-gold').parent().after($('
').append(tip.clone()));
    if ($('.link').length === 1) { // Viewing a submission?
        $('.link ul.buttons').append($('
').append(tip.clone()));
    }
}

/* Tipping button functionality. */
$('.tip-bitcoins').bind('click', function(event) {
    var $target = $(event.target);
    var form = null;
    if ($target.closest('.link').length > 0) {
        /* Post */
        form = $('.usertext-edit').first();
    } else {
        /* Comment */
        reddit.reply(event.target);
        form = reddit.comment_reply_for_elem(event.target);
    }
    var textarea = form.find('textarea');
    if (!textarea.val().match(tipregex)) {
        var insert = '+tip ' + baseTip;
        if (textarea.val().length > 0) {
            insert = '\n\n' + insert;
        } else {
            insert = insert + '\n\n';
        }
        textarea.val(textarea.val() + insert);
    }
    return false;
});

/* Reddit jQuery plugin. */
(function($) {
    /** Get the comment div for each element in the current set. */
    $.fn.comment = function() {
        return this.closest('.comment');
    };

    /** Get the comment ID for the first selected comment. */
    $.fn.commentID = function() {
        var full = this.first().find('input[name="thing_id"]').first().val();
        return full.replace(/^t1_/, '');
    };

    /** Get the commenter/poster name for the first selected comment. */
    $.fn.thingName = function() {
        return this.first().find('.author').first().text();
    };

    /** Get the comment's post time for the first selected comment. */
    $.fn.commentDate = function() {
        return new Date(this.find('.tagline time').first().attr('datetime'));
    };

    /** Get the comment body for the first comment in the current set. */
    $.fn.commentBody = function() {
        return this.find('.md').first();
    };

    /** Get the children comments for each comment in the current set. */
    $.fn.commentChildren = function() {
        return this.find('.comment');
    };

    /** Get the parent comment for each comment in the current set. */
    $.fn.commentParent = function() {
        return this.parent().comment();
    };

    /** Determine whether the first element is the target comment. */
    $.fn.isTarget = function() {
        return this.first().find('form').first().hasClass('border');
    };

    /** Return true if the first comment in the current set has a tip. */
    $.fn.hasTip = function() {
        return this.commentBody().children().is(function() {
            return tipregex.test($(this).text());
        });
    };

    /** Return the link ID for the first post in the selection. */
    $.fn.postID = function() {
        return this.attr('data-fullname').replace(/^t._/, '');
    };

    /** Return true if the first seleted item is a comment. */
    $.fn.isComment = function() {
        return this.closest('.link').length === 0;
    };
})(unsafeWindow.jQuery);

/* Hide verification replies. Note: t2_7vw3n is /u/bitcointip. */
$('a.id-t2_7vw3n').comment().each(function() {
    var $this = $(this);
    if ($this.commentChildren().length === 0 && !$this.isTarget()) {
        reddit.hidecomment($this.find('.expand').first());
    }
});

/* Find all the tip comments. */
var tips = {};
$('div.comment').each(function() {
    var $this = $(this);
    if ($this.hasTip()) {
        tips[$this.commentID()] = $this;
    }
});

/* Get status information about various tips. */
var inTipSubreddit = /^\/r\/bitcointip/.test(document.location.pathname);
var tipIDs = Object.keys(tips);
var confirmedIDs = [];
if (tipIDs.length > 0 || inTipSubreddit) {
    var iconStyle = 'vertical-align: text-bottom; margin-left: 8px;';
    var display = {
        "pending": icons.verified,
        "completed": icons.verified,
        "reversed": icons.verified,
        "cancelled": icons.rejected
    };
    $.getJSON(api.gettips + 'tips=' + tipIDs, function(response) {
        var lastEvaluated = new Date(response.last_evaluated * 1000);
        if (inTipSubreddit) {
            var botStatus = null;
            if (Date.now() - lastEvaluated > botDownThreshold) {
                botStatus = botStatusHtml.down;
            } else {
                botStatus = botStatusHtml.up;
            }
            $('a[href="http://bitcointip.net/status.php"]').html(botStatus);
        }

        response.tips.forEach(function (tip) {
            var id = tip.fullname.replace(/^t._/, '');
            var tagline = tips[id].find('.tagline').first();
            var icon = $('').attr({href: tip.tx, target: '_blank'});
            tagline.append(icon.append($('').attr({
                src: display[tip.status],
                style: iconStyle,
                title: '+$' + tip.amountUSD + '	→  ' + tip.receiver
            })));
            confirmedIDs.push(id);
            tips[id].attr('id', 't1_' + id);
            delete tips[id];
        });

        /* Deal with unanswered tips. */
        for (var id in tips) {
            if (tips[id].commentDate() < lastEvaluated) {
                var tagline = tips[id].find('.tagline').first();
                tagline.append($('').attr({
                    src: icons.rejected,
                    style: iconStyle,
                    title: 'This tip is invalid.'
                }));
            }
        }
    });

    /* Put receiver information on comments. */
    var things = {};
    $('div.comment').each(function() {
        var $this = $(this);
        things[$this.commentID()] = $this;
    });
    $('div.link').each(function() {
        var $this = $(this);
        things[$this.postID()] = $this;
    });
    var thingIDs = Object.keys(things);
    $.getJSON(api.gettipped + 'tipped=' + thingIDs, function(response) {
        response.forEach(function (tipped) {
            var id = tipped.fullname.replace(/^t._/, '');
            var thing = things[id];
            var tagline = thing.find('.tagline').first();
            var plural = tipped.tipQTY > 1;
            var title = '$' + tipped.amountUSD + ' to ' + thing.thingName() +
                    ' for this ';
            if (plural) {
                title = 'redditors have given ' + title;
            } else {
                title = 'a redditor has given ' + title;
            }
            if (thing.isComment()) {
                title += 'comment.';
            } else {
                title += 'submission.';
            }
            var icon = $('').attr({
                src: icons.tipped,
                style: iconStyle,
                title: title
            });

            /* Attempt to link to the first tip. */
            var foundParent = false;
            thing.commentChildren().each(function() {
                if (!foundParent) {
                    var $this = $(this);
                    var id = $this.commentID();
                    if ($.inArray(id, confirmedIDs) >= 0) {
                        icon = $('').attr({
                            href: '#t1_' + $this.commentID()
                        }).append(icon);
                        foundParent = true;
                    }
                }
            });

            tagline.append(icon);
            if (plural) {
                tagline.append('x' + tipped.tipQTY);
            }
        });
    });
}

/* Test URLs:
 *
 * Rejected,
 *   http://www.reddit.com/r/bitcointip/comments/132nhq/t/c7c7iue
 *
 * Rejected flip,
 *   http://www.reddit.com/r/Bitcoin/comments/14i9e7/y/c7dc6w9
 *
 * Combination folding,
 *   http://www.reddit.com/r/bitcointip/comments/13iykn/b/c7dj8ia
 *
 * Multiple tips to one receiver,
 *   http://www.reddit.com/r/bitcointip/comments/12lmut/c7ny177
 *
 * Multiple guilded to one receiver (for comparison),
 *   http://www.reddit.com/r/AdviceAnimals/comments/15mk25/c7ntrcc
 */
<commit_msg>Bump version number.<commit_after>// ==UserScript==
// @name         Extra Bitcointip Support on Reddit
// @description  Add a tipping button (/u/bitcointip) and shrinks verifications
// @version      1.3.5
// @license      Public Domain
// @include      http://*.reddit.com/*
// @exclude      http://*.reddit.com/user/bitcointip
// ==/UserScript==

/* Changelog:
 * 1.3.5
 *    Drop JSONP for CORS to increase safety.
 *    Attempt to link to source tip from tipped icons.
 * 1.3.4
 *    Inject the bot status into r/bitcointip's sidebar
 * 1.3.3
 *    Fix API URL.
 *    Finer tip rejection control
 *    Small bug fixes
 * 1.3.1
 *    A tip button on posts
 *    Verification icons on posts
 *    Small display tweaks
 * 1.3.0
 *    New verification and rejection markings
 *    Use the new gettipped API
 *    All new jQuery reddit plugin
 * 1.2.1
 *    More efficient validation marking
 *    Fixed status names
 *    Run on all of reddit, including user pages
 * 1.2
 *    Use the new bitcointip API to get status information
 *    https://github.com/NerdfighterSean/bitcointip/blob/master/src/api/
 * 1.1.4
 *    Don't hide verification when it has children
 *    Don't hide verification when it's the URL target
 * 1.1.3
 *    Fix case insensitivity in regex
 * 1.1.2
 *    Display tipping amount
 * 1.1.1
 *    Support the full tipping syntax
 * 1.1
 *    Hide verification posts, inlining the verification
 * 1.0
 *    Add a "tip bitcoins" button.
 */

var baseTip = '0.01 BTC';
var tipregex = /((\+(bitcointip|bitcoin|tip|btctip|bittip|btc))|((\+((?!0)(\d{1,4})) internet(s)?)|(\+((?!0)(\d{1,4})) point(s)? to (Gryffindor|Slytherin|Ravenclaw|Hufflepuff))))/i;
var botDownThreshold = 15 * 60 * 1000; // milliseconds
var botStatusHtml = {
    up: 'UP',
    down: 'DOWN'
};
var api = {
    gettips: 'http://bitcointip.net/api/gettips.php?',
    gettipped: 'http://bitcointip.net/api/gettipped.php?'
};
var icons = {
    verified: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAMAAABFNRROAAAAt1BMVEX///8AAAAAyAAAuwAAwQcAvAcAvwAAwQYAyAUAxAUAxwQAwgQAvAMAxQYAvwYAxQYAxwU5yT060j460j871T89wUE9wkFGokdGu0hIzExJl09JmE9JxExJxE1K1U9K1k5Ll09LmVNMmVNM2FBNmlRRx1NSzlRTqlVUslZU1ldVq1hVrFdV2FhWrFhX21pZqlphrWJh3WRotGtrqm1stW91sXd2t3h5t3urz6zA2sHA28HG3sf4+PhvgZhQAAAAEXRSTlMAARweJSYoLTM0O0dMU1dYbkVIv+oAAACKSURBVHjaVc7XEoIwEIXhFRED1tBUxBaPFSyxK3n/5zIBb/yv9pudnVky2Ywxm345MHkVXByllPm4W24qrLbzdo1sLPPRepc+XlnSIAuz9DQYPtXnkLhUF/ysrndV3CYLRpbg2VtpxFMwfRfEl8IghEPUhB9t9lEQoke6FnzONfpU5kEIoKOn/z+/pREPWTic38sAAAAASUVORK5CYII=",
    rejected: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAMAAABFNRROAAAAQlBMVEX///+qAAAAAAC/AADIABSaTU3YMDDcPj7cSEjeUFDiZGTld3fmfHzoiorqkJDqlpbupKTuqqr99fX99/f+/Pz////kWqLlAAAABXRSTlMAAwcIM6KYVMQAAABfSURBVHjaXc7JDsAgCEVRsYpIBzro//9qHyHpond3AgkklIuXPKJcqleIIEB6FwEhQEW0t4rlUtt+22ZTMQ09NqZyiK8BtBCvc9iDWegY526hBVRmdcQ9RgD9f/G+P1+JEwRF2vKhRgAAAABJRU5ErkJggg==",
    tipped: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAANCAMAAACq939wAAAA/FBMVEWqVQCqcQCZZgCLXQCdYgDMjACOXACOXwCacQCfcQCqegCwggChggCnfwCwggCthACtgQC9iACleQC+iwDMlgDJlACmfgDDiwDBjADHlQDJnQDFlQDKmAChfRGjgBOmfhKmghKnghOqhBKthxOviROvjB+vjCGvjCOwiRSwihixixWxjSGziBOzkSmzky+0kSa0kiy3jxW8kxS8mCi8n0i8oEq9lBe9mSvOoBbUrTTUrTjbukXcsTDctDrdtj/exHbexXDfwWXfwmvjrBnksRjksx7lrhnqx17qyWTqymrq377rz3nr2qftuiHtvSv67cD67sj+997/+OX///8rcy1sAAAAHXRSTlMDCQoLDRQkKystMDc5Oj0+QUlKS0tMTVFSV15/i6wTI/gAAACWSURBVHjaHcrnAoFQGADQryI7sjNKN0RKZJVNQ8io938YN+f3ASDp1B9NAhD15UzXNH26KhNQXZyDBxZcNlloKadvFIbR56owUOFV9425ai8PrGwZaITQeisXoKHs7k/MP44ZaHYl54U5El8EdmjNEWbEjesf/Ljd9v0S1ETBtD3PNgUxB8nOQIybOGknAKgMy2FsmoIflIEZdK7PshkAAAAASUVORK5CYII="
};
var $ = unsafeWindow.$, reddit = unsafeWindow;

/* Add the "tip bitcoins" button after "give gold". */
var tip = $('tip bitcoins').attr({
    'class': 'tip-bitcoins login-required',
    'href': '#'
});
if (/^\/r\//.test(document.location.pathname)) {
    $('a.give-gold').parent().after($('
').append(tip.clone()));
    if ($('.link').length === 1) { // Viewing a submission?
        $('.link ul.buttons').append($('
').append(tip.clone()));
    }
}

/* Tipping button functionality. */
$('.tip-bitcoins').bind('click', function(event) {
    var $target = $(event.target);
    var form = null;
    if ($target.closest('.link').length > 0) {
        /* Post */
        form = $('.usertext-edit').first();
    } else {
        /* Comment */
        reddit.reply(event.target);
        form = reddit.comment_reply_for_elem(event.target);
    }
    var textarea = form.find('textarea');
    if (!textarea.val().match(tipregex)) {
        var insert = '+tip ' + baseTip;
        if (textarea.val().length > 0) {
            insert = '\n\n' + insert;
        } else {
            insert = insert + '\n\n';
        }
        textarea.val(textarea.val() + insert);
    }
    return false;
});

/* Reddit jQuery plugin. */
(function($) {
    /** Get the comment div for each element in the current set. */
    $.fn.comment = function() {
        return this.closest('.comment');
    };

    /** Get the comment ID for the first selected comment. */
    $.fn.commentID = function() {
        var full = this.first().find('input[name="thing_id"]').first().val();
        return full.replace(/^t1_/, '');
    };

    /** Get the commenter/poster name for the first selected comment. */
    $.fn.thingName = function() {
        return this.first().find('.author').first().text();
    };

    /** Get the comment's post time for the first selected comment. */
    $.fn.commentDate = function() {
        return new Date(this.find('.tagline time').first().attr('datetime'));
    };

    /** Get the comment body for the first comment in the current set. */
    $.fn.commentBody = function() {
        return this.find('.md').first();
    };

    /** Get the children comments for each comment in the current set. */
    $.fn.commentChildren = function() {
        return this.find('.comment');
    };

    /** Get the parent comment for each comment in the current set. */
    $.fn.commentParent = function() {
        return this.parent().comment();
    };

    /** Determine whether the first element is the target comment. */
    $.fn.isTarget = function() {
        return this.first().find('form').first().hasClass('border');
    };

    /** Return true if the first comment in the current set has a tip. */
    $.fn.hasTip = function() {
        return this.commentBody().children().is(function() {
            return tipregex.test($(this).text());
        });
    };

    /** Return the link ID for the first post in the selection. */
    $.fn.postID = function() {
        return this.attr('data-fullname').replace(/^t._/, '');
    };

    /** Return true if the first seleted item is a comment. */
    $.fn.isComment = function() {
        return this.closest('.link').length === 0;
    };
})(unsafeWindow.jQuery);

/* Hide verification replies. Note: t2_7vw3n is /u/bitcointip. */
$('a.id-t2_7vw3n').comment().each(function() {
    var $this = $(this);
    if ($this.commentChildren().length === 0 && !$this.isTarget()) {
        reddit.hidecomment($this.find('.expand').first());
    }
});

/* Find all the tip comments. */
var tips = {};
$('div.comment').each(function() {
    var $this = $(this);
    if ($this.hasTip()) {
        tips[$this.commentID()] = $this;
    }
});

/* Get status information about various tips. */
var inTipSubreddit = /^\/r\/bitcointip/.test(document.location.pathname);
var tipIDs = Object.keys(tips);
var confirmedIDs = [];
if (tipIDs.length > 0 || inTipSubreddit) {
    var iconStyle = 'vertical-align: text-bottom; margin-left: 8px;';
    var display = {
        "pending": icons.verified,
        "completed": icons.verified,
        "reversed": icons.verified,
        "cancelled": icons.rejected
    };
    $.getJSON(api.gettips + 'tips=' + tipIDs, function(response) {
        var lastEvaluated = new Date(response.last_evaluated * 1000);
        if (inTipSubreddit) {
            var botStatus = null;
            if (Date.now() - lastEvaluated > botDownThreshold) {
                botStatus = botStatusHtml.down;
            } else {
                botStatus = botStatusHtml.up;
            }
            $('a[href="http://bitcointip.net/status.php"]').html(botStatus);
        }

        response.tips.forEach(function (tip) {
            var id = tip.fullname.replace(/^t._/, '');
            var tagline = tips[id].find('.tagline').first();
            var icon = $('').attr({href: tip.tx, target: '_blank'});
            tagline.append(icon.append($('').attr({
                src: display[tip.status],
                style: iconStyle,
                title: '+$' + tip.amountUSD + '	→  ' + tip.receiver
            })));
            confirmedIDs.push(id);
            tips[id].attr('id', 't1_' + id);
            delete tips[id];
        });

        /* Deal with unanswered tips. */
        for (var id in tips) {
            if (tips[id].commentDate() < lastEvaluated) {
                var tagline = tips[id].find('.tagline').first();
                tagline.append($('').attr({
                    src: icons.rejected,
                    style: iconStyle,
                    title: 'This tip is invalid.'
                }));
            }
        }
    });

    /* Put receiver information on comments. */
    var things = {};
    $('div.comment').each(function() {
        var $this = $(this);
        things[$this.commentID()] = $this;
    });
    $('div.link').each(function() {
        var $this = $(this);
        things[$this.postID()] = $this;
    });
    var thingIDs = Object.keys(things);
    $.getJSON(api.gettipped + 'tipped=' + thingIDs, function(response) {
        response.forEach(function (tipped) {
            var id = tipped.fullname.replace(/^t._/, '');
            var thing = things[id];
            var tagline = thing.find('.tagline').first();
            var plural = tipped.tipQTY > 1;
            var title = '$' + tipped.amountUSD + ' to ' + thing.thingName() +
                    ' for this ';
            if (plural) {
                title = 'redditors have given ' + title;
            } else {
                title = 'a redditor has given ' + title;
            }
            if (thing.isComment()) {
                title += 'comment.';
            } else {
                title += 'submission.';
            }
            var icon = $('').attr({
                src: icons.tipped,
                style: iconStyle,
                title: title
            });

            /* Attempt to link to the first tip. */
            var foundParent = false;
            thing.commentChildren().each(function() {
                if (!foundParent) {
                    var $this = $(this);
                    var id = $this.commentID();
                    if ($.inArray(id, confirmedIDs) >= 0) {
                        icon = $('').attr({
                            href: '#t1_' + $this.commentID()
                        }).append(icon);
                        foundParent = true;
                    }
                }
            });

            tagline.append(icon);
            if (plural) {
                tagline.append('x' + tipped.tipQTY);
            }
        });
    });
}

/* Test URLs:
 *
 * Rejected,
 *   http://www.reddit.com/r/bitcointip/comments/132nhq/t/c7c7iue
 *
 * Rejected flip,
 *   http://www.reddit.com/r/Bitcoin/comments/14i9e7/y/c7dc6w9
 *
 * Combination folding,
 *   http://www.reddit.com/r/bitcointip/comments/13iykn/b/c7dj8ia
 *
 * Multiple tips to one receiver,
 *   http://www.reddit.com/r/bitcointip/comments/12lmut/c7ny177
 *
 * Multiple guilded to one receiver (for comparison),
 *   http://www.reddit.com/r/AdviceAnimals/comments/15mk25/c7ntrcc
 */
<|endoftext|>


<commit_before>var languages;
window.isGapped = false;

$().ready(function () {

    $.capFirst = function (string) {
        return string.charAt(0).toUpperCase() + string.slice(1);
    }

    var currentLang = 'English',
    	isGapped = (typeof Droidgap === 'undefined') ? false : true;

    // Getter:  $.data(document.body, "config").langFrom
    $.data(document.body, "config", {
        langFrom: currentLang
    });

    $.data(document.body, "view", "home");

	function offlineLookup()
	{
		if(window.isGapped)
	    {
	    	// Let's check couchDB if available.

	    	// Grab to value and search for that key in couchdb.  If there, return array and search array for result.
			/*
			    $.ajax({
			        url: 'http://127.0.0.1:5984/lingua_droid/f49081f0bc5dc49e1719e92bb700065b',
			        success: function (data) {
			            $('body').children().remove().end().append(data)
			            console.log(data)
			        }
			    });
			*/
	   	}
	}

    $('#link-about').bind('click', function () {

        var view = $.data(document.body, "view");
        var out = view === 'home' ? 'home' : 'about';
        var into = view === 'home' ? 'about' : 'home';

        $('#' + out).fadeOut(300, function () {
            var $that = $(this);
            $('#' + into).fadeIn(300, function () {

                $.data(document.body, "view", into);
                $('#link-about').text($.capFirst(out));
                // FFFFUUUUU inlne style FTL
                $that.attr('style', '').addClass('counter-hide');
                $(this).removeClass('counter-hide');

            });
        });
        return false;
    });

    languages = getLangs();

    // populate the select box
    for (var lang in languages) {
        $('#langInput').append('<option value = "' + lang + '">' + lang + '</option>');
        $('#langOutput').append('<option value = "' + lang + '">' + lang + '</option>');
    }

    /***** NAMED EVENTS *****/

    $(document).bind('##TRANSLATE_TEXT##', function (e) {

		if(!navigator.onLine)
		{
			// testing offline android client...if ur offline in ur browser, not supported for prototype...
			offlineLookup();
			return;
		}

        $('#run').attr('disabled', 'disabled');
        // Todo:  Update this to reflect langFrom.
        $('#run').val('translating...');
        // TODO: Sanitize this shit.
        var input = $('#langInput').val(),
            output = $('#langOutput').val(),
            message = $('#messageFrom').val();

        translate.text({
            input: input,
            output: output
        }, message, function (result) {
            $('#run').attr('disabled', '');
            $('#run').val('Translate');

            var obj = {
                'message': message,
                'output': result,
                'from': input,
                'to': output
            }

            storeInCouch(obj);

            $('#output').val(result);

            if (window.isGapped) {
                // beep!
                navigator.notification.beep(2);
                navigator.notification.vibrate(250);
            }

        });
    });

    function storeInCouch(obj) {
        $.get("/store", obj, function (data) {
            console.log(data)
        });
    }

    function translateUi() {
        // Let's automagically update the UI to show those phrases in the appropriate language.
        // Grab all text elements on the page
        var input = $.data(document.body, "config").langFrom;
        var output = $('#langInput option:selected').val();

        // Could definitely be optimized to not send so many requests, but fuck it for now.
        $('label, input[type=button], option, textarea, p, a').each(function (i, el) {
            translate.text({
                input: input,
                output: output
            }, el[!el.innerHTML ? 'value' : 'innerHTML'], function (result) {
                try {
                    el[!el.innerHTML ? 'value' : 'innerHTML'] = result;
                }
                catch (e) {
                    //				console.log(e);
                }
            });
        });

        $.data(document.body, "config", {
            langFrom: output
        });

    }

    /**** END NAMED EVENTS ****/

    /**** BIND UI EVENTS ****/

    $('#langInput').bind('change', translateUi);

    $('#run').click(function (e) {
        $(document).trigger('##TRANSLATE_TEXT##');
    });

    $('#clear').bind('click', function (e) {
        $('textarea').text('');
        return false;
    });

    /**** END UI BIND EVENTS ****/

    $('#run').attr('disabled', '');

    // Check local storage for prefs and if not there, populate with the following:
    $('#langInput').val('English');
    $('#langOutput').val('Dutch');

    // Lose the URL bar for mobile version...
    /mobile/i.test(navigator.userAgent) && !location.hash && setTimeout(function () {
        window.scrollTo(0, 1);
    }, 1000);

    // Is the user online?
    var online = navigator.onLine;

	// Are we in a Titanium Desktop app?
	var isTitanium = typeof window.Titanium === 'object' ? true : false;

    // Chromeless dragging in Titanium Desktop app
    (function () {
        var dragging = false;

        document.onmousemove = function ()
        {
            if (!dragging || !isTitanium) return;

            Titanium.UI.currentWindow.setX(Titanium.UI.currentWindow.getX() + event.clientX - xstart);
            Titanium.UI.currentWindow.setY(Titanium.UI.currentWindow.getY() + event.clientY - ystart);

        }

        document.onmousedown = function (e)
        {
            // disallow textarea
            if (isTitanium && e.target.className !== 'box')
            {
                dragging = true;
                xstart = event.clientX;
                ystart = event.clientY;
            }
        }

        document.onmouseup = function () {
            dragging = false;
        }
    })();

});

// TODO: if this window.Phonegap exists.

window.onload = function()
{
	document.addEventListener('deviceready',function() {
		window.isGapped = true;
	},false);
}<commit_msg>Added offline client side code for couchdb.<commit_after>var languages;
window.doc = '';
window.isGapped = false;

$().ready(function () {

    $.capFirst = function (string) {
        return string.charAt(0).toUpperCase() + string.slice(1);
    }

    var currentLang = 'English',
        isGapped = (typeof Droidgap === 'undefined') ? false : true;

    // Getter:  $.data(document.body, "config").langFrom
    $.data(document.body, "config", {
        langFrom: currentLang
    });

    $.data(document.body, "view", "home");

    $('#link-about').bind('click', function () {

        var view = $.data(document.body, "view");
        var out = view === 'home' ? 'home' : 'about';
        var into = view === 'home' ? 'about' : 'home';

        $('#' + out).fadeOut(300, function () {
            var $that = $(this);
            $('#' + into).fadeIn(300, function () {

                $.data(document.body, "view", into);
                $('#link-about').text($.capFirst(out));
                // FFFFUUUUU inlne style FTL
                $that.attr('style', '').addClass('counter-hide');
                $(this).removeClass('counter-hide');

            });
        });
        return false;
    });

    languages = getLangs();

    // populate the select box
    for (var lang in languages) {
        $('#langInput').append('<option value = "' + lang + '">' + lang + '</option>');
        $('#langOutput').append('<option value = "' + lang + '">' + lang + '</option>');
    }

    /***** NAMED EVENTS *****/

    $(document).bind('##TRANSLATE_TEXT##', function (e) {

        // TODO: Sanitize this shit.
        var input = $('#langInput').val(),
            output = $('#langOutput').val(),
            message = $('#messageFrom').val();

        $('#run').attr('disabled', 'disabled');
        // Todo:  Update this to reflect langFrom.
        $('#run').val('translating...');

        if (window.isGapped) {
            // testing offline android client...if ur offline in ur browser, not supported for prototype...
            offlineLookup(input, output, message);
            return;
        }

        // otherwise, use the Google Translation API
        translate.text({
            input: input,
            output: output
        }, message, function (result) {
            $('#run').attr('disabled', '');
            $('#run').val('Translate');

            var obj = {
                'message': message,
                'output': result,
                'from': input,
                'to': output
            }

            storeInCouch(obj);

            $('#output').val(result);

            if (window.isGapped) {
                // beep!
                navigator.notification.beep(2);
                navigator.notification.vibrate(250);
            }

        });
    });

    function storeInCouch(obj) {
        $.get("/store", obj, function (data) {
            console.log(data)
        });
    }

    function offlineLookup(from, to, message) {
        if (window.isGapped) {
            getOfflineCouch(from, to, message);
        }
    }

    function getOfflineCouch(from, to, message, cb) {

        // TODO: Sanitize this
        var compoundKey = from.toLowerCase() + "_" + to.toLowerCase();
	    var words = [];
        words = message.split(" ");
        var firstword = words[0].toLowerCase();

		console.log(doc)

        console.log(firstword + " is the first word.")
       	console.log(compoundKey + " is the compound key.")

        // Does compoundKey exist?
        if (typeof doc[compoundKey] === 'undefined') {
            offlineResult(false, 'The compound key was not found.');
        }
        else {
            // Does the firstword key exist?
            if (typeof doc[compoundKey][firstword] === 'undefined') {
                offlineResult(false, 'The first word was not found.');
            }
            else {
                // iterate over the array looking for the phrase and if it exists, just update the timestamp.
                var messageExists = false;
                var index = -1;

                doc[compoundKey][firstword].forEach(function (el, i, a) {
                    if (el.from === message) {
                        messageExists = true;
                        matchedTranslatedPhrase = el.to;
                        index = i;
                    }
                });

                if (messageExists) {
                    offlineResult(true, 'The message was found.', matchedTranslatedPhrase);
                }
                else {
                    // add the new message, output and timestamp
                    offlineResult(false, 'The message was not found.');
                }
            }
        }
   }

        function offlineResult(flag, logMessage, translation) {
        	console.log(flag)
        	console.log("***************");
			console.log(logMessage)
        	console.log("***************");
            if (flag) {
                $('#output').val(translation)
                console.log(logMessage);
            }
            else {
                alert(logMessage);
                console.log(logMessage)
            }
        }

        function translateUi() {
            // Let's automagically update the UI to show those phrases in the appropriate language.
            // Grab all text elements on the page
            var input = $.data(document.body, "config").langFrom;
            var output = $('#langInput option:selected').val();

            // Could definitely be optimized to not send so many requests, but fuck it for now.
            $('label, input[type=button], option, textarea, p, a').each(function (i, el) {
                translate.text({
                    input: input,
                    output: output
                }, el[!el.innerHTML ? 'value' : 'innerHTML'], function (result) {
                    try {
                        el[!el.innerHTML ? 'value' : 'innerHTML'] = result;
                    }
                    catch (e) {
                        //				console.log(e);
                    }
                });
            });

            $.data(document.body, "config", {
                langFrom: output
            });

        }

        /**** END NAMED EVENTS ****/

        /**** BIND UI EVENTS ****/

        $('#langInput').bind('change', translateUi);

        $('#run').click(function (e) {
            $(document).trigger('##TRANSLATE_TEXT##');
        });

        $('#clear').bind('click', function (e) {
            $('textarea').text('');
            return false;
        });

        /**** END UI BIND EVENTS ****/

        $('#run').attr('disabled', '');

        // Check local storage for prefs and if not there, populate with the following:
        $('#langInput').val('English');
        $('#langOutput').val('Dutch');

        // Lose the URL bar for mobile version...
        /mobile/i.test(navigator.userAgent) && !location.hash && setTimeout(function () {
            window.scrollTo(0, 1);
        }, 1000);

        // Is the user online?
        var online = navigator.onLine;

        // Are we in a Titanium Desktop app?
        var isTitanium = typeof window.Titanium === 'object' ? true : false;

        // Chromeless dragging in Titanium Desktop app
        (function () {
            var dragging = false;

            document.onmousemove = function () {
                if (!dragging || !isTitanium) return;

                Titanium.UI.currentWindow.setX(Titanium.UI.currentWindow.getX() + event.clientX - xstart);
                Titanium.UI.currentWindow.setY(Titanium.UI.currentWindow.getY() + event.clientY - ystart);

            }

            document.onmousedown = function (e) {
                // disallow textarea
                if (isTitanium && e.target.className !== 'box') {
                    dragging = true;
                    xstart = event.clientX;
                    ystart = event.clientY;
                }
            }

            document.onmouseup = function () {
                dragging = false;
            }
        })();

    });

// TODO: if this window.Phonegap exists.
window.onload = function () {
    document.addEventListener('deviceready', function () {
    	alert( !!(device.platform) )

    if( !!(device.platform) )
    {
        window.isGapped = true;

	        $.ajax({
	            url: 'http://127.0.0.1:5984/lingua/lingua-couch',
	            success: function (data) {
	            	console.log(data)
	                doc = JSON.parse(data);
	                console.log(doc)
	            }
	        });
    }
    else
    {
    }

    }, false);
}<|endoftext|>


<commit_before>var sizeOfGPAList=GPAlist.length;
var maxCourses=6;

function calculatePercentage(marks,creditWeights){

  var percentage=0;
  var combinedCreditWeight=0;

  //calculates the percentage based on the input
  for (var i=0; i<maxCourses; i++){
    var mark=parseFloat(marks[i]);

    if (mark>=0 && mark<=100 && !isNaN(mark)){ //checks to make sure the input is valid

      var creditWeight=Number(creditWeights[i]);
      percentage+=mark*creditWeight;
      combinedCreditWeight+=creditWeight;
    }
  }
  percentage/=combinedCreditWeight;

  if (isNaN(percentage)) //makes sure the inputs were filled in
    percentage=0;

  return percentage;
}

function displayPercent(percentage){
  if (!isNaN(percentage) && percentage!=0)
    document.getElementById("percentage-output").innerHTML=Math.round(percentage*100)/100;

  else
    document.getElementById("percentage-output").innerHTML="--";
}

function displayGPA(GPA){
  GPA=Math.round(GPA*100)/100;

  if (GPA!=0 && !isNaN(GPA))
    document.getElementById("GPA-output").innerHTML=GPA;

  else
    document.getElementById("GPA-output").innerHTML="--";
}

//requires the gpa so as to check wether to display a legitimate "F" or a -- if the user just happened to not enter anything
function displayLetter(letter, GPA){

  if (GPA!=0 && !isNaN(GPA))
    document.getElementById("letter-output").innerHTML=letter;

  else
    document.getElementById("letter-output").innerHTML="--";
}

function display12Point(grade){

  if (grade!=0 && !isNaN(grade))
    document.getElementById("12point-output").innerHTML=Math.round(grade*100)/100;

  else
    document.getElementById("12point-output").innerHTML="--";
}

function convertLetterToGPA(universityIndex, marks, creditWeights){
  var GPAtotal=0;
  var creditTotal=0;
  var pointTotal=0;

  var referenceLetterGradeIndex=universities[universityIndex][2];
  //scrolls through all the letter marks
  for (var i=0; i<maxCourses; i++){

    //scrolls through the GPA list and finds a match to the mark
    for (var j = 0; j <sizeOfGPAList; j++) {

      //checks if the mark of the course matches a GPA
      if (marks[i].toUpperCase()==GPAlist[j][referenceLetterGradeIndex]){
        var creditWeight=Number(creditWeights[i]);
        GPAtotal+=GPAlist[j][0]*creditWeight; //adds the GPA to the total
        creditTotal+=creditWeight;
        pointTotal+=GPAlist[j][8]*creditWeight;
        break;
      }
    }
  }

  var GPA=GPAtotal/creditTotal;
  var points=pointTotal/creditTotal;
  if (isNaN(GPA) || isNaN(creditTotal)){//Checks to make sure that the fields were filled in
    GPA=0;
    points=0;
  }

  //calculates the 12-point system and letter grade based on the GPA
  var i;
  for (i = 0; i <sizeOfGPAList; i++) {
    if (GPA>=GPAlist[i][0]){
      break;
    }
  }

  var grades=new Array(GPA,GPAlist[i][referenceLetterGradeIndex],points);

  return grades;
}

function convertPercentToGPA(universityIndex, marks, creditWeights){
  var GPAtotal=0;
  var creditTotal=0;
  var pointTotal=0;

  var referencePercentageIndex=universities[universityIndex][1];
  var referenceLetterGradeIndex=universities[universityIndex][2];

  for (var i=0; i<maxCourses; i++){

    for (var j=0; j<sizeOfGPAList; j++){

      var mark=parseFloat(marks[i]);

      if (mark>=0 &&  mark<=100 &&!isNaN(mark) && mark>=GPAlist[j][referencePercentageIndex]){ //if input is valid and matches list
        var creditWeight=Number(creditWeights[i]);
        GPAtotal+=GPAlist[j][0]*creditWeight; //adds the GPA to the total
        creditTotal+=creditWeight;
        pointTotal+=GPAlist[j][8]*creditWeight;
        break;
      }
    }
  }

  var GPA=GPAtotal/creditTotal;
  var points=pointTotal/creditTotal;

  if (isNaN(GPA) || isNaN(points)){//Checks to make sure that the fields were filled in
    GPA=0;
    points=0;
  }

  //calculates the letter grade and 12-point system grade based on final gpa
  var i;
  for (i = 0; i <sizeOfGPAList; i++) {
    if (GPA>=GPAlist[i][0]){
      break;
    }
  }

  var grades=new Array(GPA,GPAlist[i][referenceLetterGradeIndex],points);

  return grades;
}

function convert12PointToGPA(universityIndex, marks, creditWeights){
  var GPAtotal=0;
  var creditTotal=0;
  var PointTotal=0;

  var referenceLetterGradeIndex=universities[universityIndex][2];

  for (var i=0; i<maxCourses; i++){
    for (var j=0; j<sizeOfGPAList; j++){

      var mark=parseInt(marks[i]);

      if (mark==GPAlist[j][8] && !isNaN(mark)){ //if input is valid and if it finds the input in the list

        var creditWeight=Number(creditWeights[i]);
        PointTotal+=marks[i]*creditWeight;
        GPAtotal+=GPAlist[j][0]*creditWeight; //adds the GPA to the total
        creditTotal+=creditWeight;
        break;
      }
    }
  }

  var GPA=GPAtotal/creditTotal;
  var point=PointTotal/creditTotal;
  if (isNaN(GPA) || isNaN(point)){//Checks to make sure that the fields were filled in
    GPA=0;
    point=0;
  }

  //calculates the letter grade based on the GPA
  var i;
  for (i = 0; i <sizeOfGPAList; i++) {
    if (GPA>=GPAlist[i][0]){
      break;
    }
  }

  var grades=new Array(GPA,GPAlist[i][referenceLetterGradeIndex],point);

  return grades;

}

//calculates and outputs the GPA information
function calculateGPA(){
    //Storing the values from the input fields
    var marks=new Array(document.getElementById("course_1_value").value,
      document.getElementById("course_2_value").value,
      document.getElementById("course_3_value").value,
      document.getElementById("course_4_value").value,
      document.getElementById("course_5_value").value,
      document.getElementById("course_6_value").value);

    var creditWeights=new Array (document.getElementById("course_1_weight").value,
      document.getElementById("course_2_weight").value,
      document.getElementById("course_3_weight").value,
      document.getElementById("course_4_weight").value,
      document.getElementById("course_5_weight").value,
      document.getElementById("course_6_weight").value);

    //works as long as the list in university-list.js matches the one in the html
    var universityIndex=document.getElementById("university").selectedIndex;

    //Checks for type of grades inputed
    if (document.getElementById("input-percent").checked)
      var gradeType="percent";
    else if (document.getElementById("input-letter").checked)
      var gradeType="letter";
    else
      var gradeType="12-point";

    if (gradeType=="percent"){
      var percentage=calculatePercentage(marks,creditWeights);
      var grades=convertPercentToGPA(universityIndex,marks, creditWeights);

      displayPercent(percentage);
      displayGPA(grades[0]);
      displayLetter(grades[1],grades[0]);
      display12Point(grades[2]);
    }

    else if (gradeType=="letter"){
      var grades=convertLetterToGPA(universityIndex, marks, creditWeights);
      displayPercent(0);
      displayGPA(grades[0]);
      displayLetter(grades[1],grades[0]);
      display12Point(grades[2]);
    }

    else if (gradeType=="12-point"){
      var grades=convert12PointToGPA(universityIndex, marks, creditWeights);
      displayPercent(0);
      displayGPA(grades[0]);
      displayLetter(grades[1],grades[0]);
      display12Point(grades[2]);
    }

    return false; //returns a false that stops form submission, since theres nothing to really submit to

  } //end of function<commit_msg>Added support for floating point input for 12-point grade<commit_after>var sizeOfGPAList=GPAlist.length;
var maxCourses=6;

function calculatePercentage(marks,creditWeights){

  var percentage=0;
  var combinedCreditWeight=0;

  //calculates the percentage based on the input
  for (var i=0; i<maxCourses; i++){
    var mark=parseFloat(marks[i]);

    if (mark>=0 && mark<=100 && !isNaN(mark)){ //checks to make sure the input is valid

      var creditWeight=Number(creditWeights[i]);
      percentage+=mark*creditWeight;
      combinedCreditWeight+=creditWeight;
    }
  }
  percentage/=combinedCreditWeight;

  if (isNaN(percentage)) //makes sure the inputs were filled in
    percentage=0;

  return percentage;
}

function displayPercent(percentage){
  if (!isNaN(percentage) && percentage!=0)
    document.getElementById("percentage-output").innerHTML=Math.round(percentage*100)/100;

  else
    document.getElementById("percentage-output").innerHTML="--";
}

function displayGPA(GPA){
  GPA=Math.round(GPA*100)/100;

  if (GPA!=0 && !isNaN(GPA))
    document.getElementById("GPA-output").innerHTML=GPA;

  else
    document.getElementById("GPA-output").innerHTML="--";
}

//requires the gpa so as to check wether to display a legitimate "F" or a -- if the user just happened to not enter anything
function displayLetter(letter, GPA){

  if (GPA!=0 && !isNaN(GPA))
    document.getElementById("letter-output").innerHTML=letter;

  else
    document.getElementById("letter-output").innerHTML="--";
}

function display12Point(grade){

  if (grade!=0 && !isNaN(grade))
    document.getElementById("12point-output").innerHTML=Math.round(grade*100)/100;

  else
    document.getElementById("12point-output").innerHTML="--";
}

function convertLetterToGPA(universityIndex, marks, creditWeights){
  var GPAtotal=0;
  var creditTotal=0;
  var pointTotal=0;

  var referenceLetterGradeIndex=universities[universityIndex][2];
  //scrolls through all the letter marks
  for (var i=0; i<maxCourses; i++){

    //scrolls through the GPA list and finds a match to the mark
    for (var j = 0; j <sizeOfGPAList; j++) {

      //checks if the mark of the course matches a GPA
      if (marks[i].toUpperCase()==GPAlist[j][referenceLetterGradeIndex]){
        var creditWeight=Number(creditWeights[i]);
        GPAtotal+=GPAlist[j][0]*creditWeight; //adds the GPA to the total
        creditTotal+=creditWeight;
        pointTotal+=GPAlist[j][8]*creditWeight;
        break;
      }
    }
  }

  var GPA=GPAtotal/creditTotal;
  var points=pointTotal/creditTotal;
  if (isNaN(GPA) || isNaN(creditTotal)){//Checks to make sure that the fields were filled in
    GPA=0;
    points=0;
  }

  //calculates the 12-point system and letter grade based on the GPA
  var i;
  for (i = 0; i <sizeOfGPAList; i++) {
    if (GPA>=GPAlist[i][0]){
      break;
    }
  }

  var grades=new Array(GPA,GPAlist[i][referenceLetterGradeIndex],points);

  return grades;
}

function convertPercentToGPA(universityIndex, marks, creditWeights){
  var GPAtotal=0;
  var creditTotal=0;
  var pointTotal=0;

  var referencePercentageIndex=universities[universityIndex][1];
  var referenceLetterGradeIndex=universities[universityIndex][2];

  for (var i=0; i<maxCourses; i++){

    for (var j=0; j<sizeOfGPAList; j++){

      var mark=parseFloat(marks[i]);

      if (mark>=0 &&  mark<=100 &&!isNaN(mark) && mark>=GPAlist[j][referencePercentageIndex]){ //if input is valid and matches list
        var creditWeight=Number(creditWeights[i]);
        GPAtotal+=GPAlist[j][0]*creditWeight; //adds the GPA to the total
        creditTotal+=creditWeight;
        pointTotal+=GPAlist[j][8]*creditWeight;
        break;
      }
    }
  }

  var GPA=GPAtotal/creditTotal;
  var points=pointTotal/creditTotal;

  if (isNaN(GPA) || isNaN(points)){//Checks to make sure that the fields were filled in
    GPA=0;
    points=0;
  }

  //calculates the letter grade and 12-point system grade based on final gpa
  var i;
  for (i = 0; i <sizeOfGPAList; i++) {
    if (GPA>=GPAlist[i][0]){
      break;
    }
  }

  var grades=new Array(GPA,GPAlist[i][referenceLetterGradeIndex],points);

  return grades;
}

function convert12PointToGPA(universityIndex, marks, creditWeights){
  var GPAtotal=0;
  var creditTotal=0;
  var PointTotal=0;

  var referenceLetterGradeIndex=universities[universityIndex][2];

  for (var i=0; i<maxCourses; i++){
    for (var j=0; j<sizeOfGPAList; j++){

      var mark=parseFloat(marks[i]);

      if (mark>=GPAlist[j][8] && !isNaN(mark) && mark<=12 && mark >=0){ //if input is valid and if it finds the input in the list

        var creditWeight=Number(creditWeights[i]);
        PointTotal+=mark*creditWeight;
        GPAtotal+=GPAlist[j][0]*creditWeight; //adds the GPA to the total
        creditTotal+=creditWeight;
        break;
      }
    }
  }

  var GPA=GPAtotal/creditTotal;
  var point=PointTotal/creditTotal;
  if (isNaN(GPA) || isNaN(point)){//Checks to make sure that the fields were filled in
    GPA=0;
    point=0;
  }

  //calculates the letter grade based on the GPA
  var i;
  for (i = 0; i <sizeOfGPAList; i++) {
    if (GPA>=GPAlist[i][0]){
      break;
    }
  }

  var grades=new Array(GPA,GPAlist[i][referenceLetterGradeIndex],point);

  return grades;

}

//calculates and outputs the GPA information
function calculateGPA(){
    //Storing the values from the input fields
    var marks=new Array(document.getElementById("course_1_value").value,
      document.getElementById("course_2_value").value,
      document.getElementById("course_3_value").value,
      document.getElementById("course_4_value").value,
      document.getElementById("course_5_value").value,
      document.getElementById("course_6_value").value);

    var creditWeights=new Array (document.getElementById("course_1_weight").value,
      document.getElementById("course_2_weight").value,
      document.getElementById("course_3_weight").value,
      document.getElementById("course_4_weight").value,
      document.getElementById("course_5_weight").value,
      document.getElementById("course_6_weight").value);

    //works as long as the list in university-list.js matches the one in the html
    var universityIndex=document.getElementById("university").selectedIndex;

    //Checks for type of grades inputed
    if (document.getElementById("input-percent").checked)
      var gradeType="percent";
    else if (document.getElementById("input-letter").checked)
      var gradeType="letter";
    else
      var gradeType="12-point";

    if (gradeType=="percent"){
      var percentage=calculatePercentage(marks,creditWeights);
      var grades=convertPercentToGPA(universityIndex,marks, creditWeights);

      displayPercent(percentage);
      displayGPA(grades[0]);
      displayLetter(grades[1],grades[0]);
      display12Point(grades[2]);
    }

    else if (gradeType=="letter"){
      var grades=convertLetterToGPA(universityIndex, marks, creditWeights);
      displayPercent(0);
      displayGPA(grades[0]);
      displayLetter(grades[1],grades[0]);
      display12Point(grades[2]);
    }

    else if (gradeType=="12-point"){
      var grades=convert12PointToGPA(universityIndex, marks, creditWeights);
      displayPercent(0);
      displayGPA(grades[0]);
      displayLetter(grades[1],grades[0]);
      display12Point(grades[2]);
    }

    return false; //returns a false that stops form submission, since theres nothing to really submit to

  } //end of function<|endoftext|>


<commit_before>/*
  Markdown Block
*/
SirTrevor.Blocks.Markdown = SirTrevor.Block.extend({

  type: "markdown",

  title: "Markdown",

  editorHTML: '

',

  icon_name: 'text',

  loadData: function(data){
    this.getTextBlock().html(SirTrevor.toHTML(data.text, this.type));
  }
});<commit_msg>Fix on Markdown block<commit_after>/*
  Markdown Block
*/
SirTrevor.Blocks.Markdown = SirTrevor.Block.extend({

  type: "markdown",

  title: "Markdown",

  editorHTML: '

',

  icon_name: 'text',

  loadData: function(data) {
    this.getTextBlock().html(SirTrevor.toHTML(data.text, this.type));
  },

  toMarkdown: function(markdown) {
    return markdown.replace(/\\\*/g,"*")
      .replace(/\\\[/g,"[")
      .replace(/\\\]/g,"]")
      .replace(/\\\_/g,"_")
      .replace(/\\\(/g,"(")
      .replace(/\\\)/g,")")
      .replace(/\\\-/g,"-");
  },

  toHTML: function(html) {
    return html;
  }
});
<|endoftext|>


<commit_before>angular.module('anol.print', [])

.directive('anolPrint', ['PrintService', function(PrintService) {
    return {
        restrict: 'A',
        transclude: true,
        templateUrl: 'anol/modules/print/templates/print.html',
        scope: {},
        link: function(scope, element, attrs) {
            scope.isPrintable = function() {
                // TODO refactor
                return scope.pageSize !== undefined && scope.pageSize[0] !== undefined && scope.pageSize[1] !== undefined && scope.scale !== undefined;
            };
            scope.startPrint = function() {
                console.warn('Implement me');
            };
            scope.setPageSize = function(size) {
                scope.pageSize = angular.copy(size);
                scope.updatePrintLayer();
            };
            scope.updatePrintLayer = function() {
                if(scope.isPrintable()) {
                    PrintService.addFeatureFromPageSize(scope.pageSize, scope.scale);
                }
            };
            scope.resetPrintArea = function() {
                if(scope.isPrintable()) {
                    PrintService.createPrintArea(scope.pageSize, scope.scale);
                }
            };
        },
        controller: function($scope, $element, $attrs) {
            // if we assign pageSize = value in template angular put only a reverence
            // into scope.pageSize and typing somethink into width/height input fields
            // will result in modifying selected availablePageSize value

            $scope.isPageSize = function(size) {
                return angular.equals(size, $scope.pageSize);
            };

            $scope.outputFormats = PrintService.outputFormats;
            $scope.pageSizes = PrintService.pageSizes;
            $scope.scale = angular.copy(PrintService.defaultScale);
            $scope.streetIndex = false;
            $scope.licenceAgreed = false;

            $scope.outputFormat = $scope.outputFormats[0];

            $scope.$watch(
                function() {
                    return PrintService.currentPageSize;
                },
                function(newVal, oldVal) {
                    $scope.pageSize = newVal;
                }
            );
        }
    };
}]);
<commit_msg>Moved comment<commit_after>angular.module('anol.print', [])

.directive('anolPrint', ['PrintService', function(PrintService) {
    return {
        restrict: 'A',
        transclude: true,
        templateUrl: 'anol/modules/print/templates/print.html',
        scope: {},
        link: function(scope, element, attrs) {
            scope.isPrintable = function() {
                // TODO refactor
                return scope.pageSize !== undefined && scope.pageSize[0] !== undefined && scope.pageSize[1] !== undefined && scope.scale !== undefined;
            };
            scope.startPrint = function() {
                console.warn('Implement me');
            };
            // if we assign pageSize = value in template angular put only a reverence
            // into scope.pageSize and typing somethink into width/height input fields
            // will result in modifying selected availablePageSize value
            scope.setPageSize = function(size) {
                scope.pageSize = angular.copy(size);
                scope.updatePrintLayer();
            };
            scope.updatePrintLayer = function() {
                if(scope.isPrintable()) {
                    PrintService.addFeatureFromPageSize(scope.pageSize, scope.scale);
                }
            };
            scope.resetPrintArea = function() {
                if(scope.isPrintable()) {
                    PrintService.createPrintArea(scope.pageSize, scope.scale);
                }
            };
        },
        controller: function($scope, $element, $attrs) {

            $scope.isPageSize = function(size) {
                return angular.equals(size, $scope.pageSize);
            };

            $scope.outputFormats = PrintService.outputFormats;
            $scope.pageSizes = PrintService.pageSizes;
            $scope.scale = angular.copy(PrintService.defaultScale);
            $scope.streetIndex = false;
            $scope.licenceAgreed = false;

            $scope.outputFormat = $scope.outputFormats[0];

            $scope.$watch(
                function() {
                    return PrintService.currentPageSize;
                },
                function(newVal, oldVal) {
                    $scope.pageSize = newVal;
                }
            );
        }
    };
}]);
<|endoftext|>


<commit_before>/**
 * Schedules view. Displays periods of selected courses in a week grid.
 * @param {Object} data - Object containing schedule data as returned from the API
 * @return {undefined}
 * @memberOf Yacs.views
 */
Yacs.views.schedule = function (target) {
  target.innerHTML = HandlebarsTemplates.schedules();

  var scheduleElement = target.querySelector('#schedule-container');
  var selectionElement = target.querySelector('#selection-container')
  var leftSwitchElement = target.querySelector('#left-switch');
  var rightSwitchElement = target.querySelector('#right-switch');
  var clearButtonElement = target.querySelector('#clear-btn');
  var scheduleNumElement = target.querySelector('#schedule-num');
  var scheduleCountElement = target.querySelector('#schedule-count');
  var scheduleStatusElement = target.querySelector('#schedule-status');
  var crnListElement = target.querySelector('#crn-list');
  var schedule = new Schedule(scheduleElement);
  var scheduleData = [];
  var scheduleIndex = 0;

  /**
   * Convert military time string to minutes-since-midnight integer form.
   * This function should be deprecated in issue #102
   */
  var toMinutes = function (timeString) {
    var int = parseInt(timeString);
    return Math.floor(int / 100) * 60 + int % 100;
  }

  /**
   * Translate schedules returned by the API into a displayable form.
   * For each schedule, convert period times ro minutes-since-midnight form,
   * and collect the CRNs from each section.
   * Additionally, determine the min star time and the max end time
   * of all of the schedules.
   */
  var processSchedules = function (schedules) {
    var start = 480;
    var end = 1200;
    var processedSchedules = schedules.map(function (schedule) {
      var courseIds = [];
      var events = [];
      var crns = [];
      schedule.sections.forEach(function (section) {
        var color = courseIds.indexOf(section.course_id);
        if (color == -1) {
          courseIds.push(section.course_id);
          color = courseIds.length - 1;
        }
        crns.push(section.crn)
        section.periods.forEach(function (period) {
          start = Math.min(start, toMinutes(period.start));
          end = Math.max(end, toMinutes(period.end));
          events.push({
            start: toMinutes(period.start),
            end: toMinutes(period.end),
            day: period.day,
            colorNum: color,
            title: [
              section.department_code + ' ' + section.course_number + ' - ' + section.name,
              section.crn,
              section.instructors[0] || ''
            ]
          });
        });
      });
      return { events: events, crns: crns };
    });
    return { schedules: processedSchedules, start: start, end: end };
  };

  /**
   * Process schedules returned by the API, and update view accordingly,
   * setting the time range of the schedule view to accomodate all schedules.
   * If no schedules are available, show the appropriate status text.
   */
  var setSchedules = function (schedules) {
    var data = processSchedules(schedules);
    scheduleData = data.schedules;
    schedule.destroy();
    schedule = new Schedule(scheduleElement,
      { timeBegin: Math.ceil((data.start) / 60) * 60,
        timeSpan: Math.ceil((data.end - data.start) / 60) * 60 });
    scheduleCountElement.textContent = scheduleData.length;
    if (scheduleData.length > 0) {
      show(0);
    } else {
      show(-1);
      if (Yacs.user.getSelections().length > 0) {
        scheduleStatusElement.textContent = "No schedules found :( Try removing some courses";
      } else {
        scheduleStatusElement.textContent = "Nothing to see here :) Try adding some courses";
      }
    }
  };

  /**
   * Query the server for schedules based on the stored selections,
   * and update the view to show the new schedules.
   * If no sections are selected, skip the call and show nil schedules.
   */
  var updateSchedules = function () {
    var selections = Yacs.user.getSelectionsRaw();
    if (selections.length > 0) {
      Yacs.models.schedules.query({ section_ids: selections,
                                    show_periods: true },
        function(data, success) {
          if (success)
            setSchedules(data.schedules);
      });
      clearButtonElement.disabled = false;
    } else {
      setSchedules([]);
      clearButtonElement.disabled = true;
    }
  };

  /**
   * Show schdule at given index, and display corresponding CRNs.
   * If index is -1, show nil schedule.
   */
  var show = function (index) {
    if (index == -1) {
      scheduleStatusElement.textContent = "";
      scheduleNumElement.textContent = 0;
    } else {
      schedule.setEvents(scheduleData[index].events)
      scheduleNumElement.textContent = index + 1;
      scheduleStatusElement.textContent = 'CRNs: ' + scheduleData[index].crns.join(', ');
    }
  };

  /**
   * Switch to schedule [[n + 1] % n] in the sequence
   */
  var next = function () {
    scheduleIndex = (++scheduleIndex < scheduleData.length ? scheduleIndex : 0);
    show(scheduleIndex);
  }

  /**
   * Switch to schedule [[n - 1] % n] in the sequence
   */
  var previous = function () {
    scheduleIndex = (--scheduleIndex < 0 ? scheduleData.length - 1 : scheduleIndex);
    show(scheduleIndex);
  }

  /**
   * Show next schedule if right is clicked or pressed,
   * show previous schedule if left is clicked or pressed
   */
  Yacs.on('click', leftSwitchElement, previous);
  Yacs.on('click', rightSwitchElement, next);
  Yacs.on('keydown', document, function (elem, event) { if (event.keyCode == 37) previous(); });
  Yacs.on('keydown', document, function (elem, event) { if (event.keyCode == 39) next(); });

  /**
   * Clear selections in cookie when clear button is pressed, and update
   * schedule and selctions views accordingly.
   */
  Yacs.on('click', clearButtonElement, function () {
    Yacs.user.clearSelections();
    updateSchedules();
    target.querySelectorAll('course-info').forEach(function (ci) {
      ci.classList.remove('selected');
    });
    target.querySelectorAll('section').forEach(function (s) {
      s.classList.remove('selected');
    });
  });

  /**
   * Show selected courses / sections on the schedule page.
   * Update schedules view when a course is clicked.
   * TODO: Use native event handling to update views when selection changes
   */
  var selections = Yacs.user.getSelections();
  if (selections.length > 0) {
    Yacs.models.courses.query({ section_id: selections.join(','),
                                show_sections: true,
                                show_periods: true },
      function (data, success) {
        if (success) {
          Yacs.views.courses(selectionElement, data);
          target.querySelectorAll('course').forEach(function (course) {
            Yacs.on('click', course, updateSchedules);
          });
        }
    });
  }

  updateSchedules();
};
<commit_msg>fix js error when navigating through no schedules<commit_after>/**
 * Schedules view. Displays periods of selected courses in a week grid.
 * @param {Object} data - Object containing schedule data as returned from the API
 * @return {undefined}
 * @memberOf Yacs.views
 */
Yacs.views.schedule = function (target) {
  target.innerHTML = HandlebarsTemplates.schedules();

  var scheduleElement = target.querySelector('#schedule-container');
  var selectionElement = target.querySelector('#selection-container')
  var leftSwitchElement = target.querySelector('#left-switch');
  var rightSwitchElement = target.querySelector('#right-switch');
  var clearButtonElement = target.querySelector('#clear-btn');
  var scheduleNumElement = target.querySelector('#schedule-num');
  var scheduleCountElement = target.querySelector('#schedule-count');
  var scheduleStatusElement = target.querySelector('#schedule-status');
  var crnListElement = target.querySelector('#crn-list');
  var schedule = new Schedule(scheduleElement);
  var scheduleData = [];
  var scheduleIndex = 0;

  /**
   * Convert military time string to minutes-since-midnight integer form.
   * This function should be deprecated in issue #102
   */
  var toMinutes = function (timeString) {
    var int = parseInt(timeString);
    return Math.floor(int / 100) * 60 + int % 100;
  }

  /**
   * Translate schedules returned by the API into a displayable form.
   * For each schedule, convert period times ro minutes-since-midnight form,
   * and collect the CRNs from each section.
   * Additionally, determine the min star time and the max end time
   * of all of the schedules.
   */
  var processSchedules = function (schedules) {
    var start = 480;
    var end = 1200;
    var processedSchedules = schedules.map(function (schedule) {
      var courseIds = [];
      var events = [];
      var crns = [];
      schedule.sections.forEach(function (section) {
        var color = courseIds.indexOf(section.course_id);
        if (color == -1) {
          courseIds.push(section.course_id);
          color = courseIds.length - 1;
        }
        crns.push(section.crn)
        section.periods.forEach(function (period) {
          start = Math.min(start, toMinutes(period.start));
          end = Math.max(end, toMinutes(period.end));
          events.push({
            start: toMinutes(period.start),
            end: toMinutes(period.end),
            day: period.day,
            colorNum: color,
            title: [
              section.department_code + ' ' + section.course_number + ' - ' + section.name,
              section.crn,
              section.instructors[0] || ''
            ]
          });
        });
      });
      return { events: events, crns: crns };
    });
    return { schedules: processedSchedules, start: start, end: end };
  };

  /**
   * Process schedules returned by the API, and update view accordingly,
   * setting the time range of the schedule view to accomodate all schedules.
   * If no schedules are available, show the appropriate status text.
   */
  var setSchedules = function (schedules) {
    var data = processSchedules(schedules);
    scheduleData = data.schedules;
    schedule.destroy();
    schedule = new Schedule(scheduleElement,
      { timeBegin: Math.ceil((data.start) / 60) * 60,
        timeSpan: Math.ceil((data.end - data.start) / 60) * 60 });
    scheduleCountElement.textContent = scheduleData.length;
    if (scheduleData.length > 0) {
      show(0);
    } else {
      show(-1);
      if (Yacs.user.getSelections().length > 0) {
        scheduleStatusElement.textContent = "No schedules found :( Try removing some courses";
      } else {
        scheduleStatusElement.textContent = "Nothing to see here :) Try adding some courses";
      }
    }
  };

  /**
   * Query the server for schedules based on the stored selections,
   * and update the view to show the new schedules.
   * If no sections are selected, skip the call and show nil schedules.
   */
  var updateSchedules = function () {
    var selections = Yacs.user.getSelectionsRaw();
    if (selections.length > 0) {
      Yacs.models.schedules.query({ section_ids: selections,
                                    show_periods: true },
        function(data, success) {
          if (success)
            setSchedules(data.schedules);
      });
      clearButtonElement.disabled = false;
    } else {
      setSchedules([]);
      clearButtonElement.disabled = true;
    }
  };

  /**
   * Show schdule at given index, and display corresponding CRNs.
   * If index is -1, show nil schedule.
   */
  var show = function (index) {
    if (index == -1) {
      scheduleStatusElement.textContent = "";
      scheduleNumElement.textContent = 0;
    } else {
      schedule.setEvents(scheduleData[index].events)
      scheduleNumElement.textContent = index + 1;
      scheduleStatusElement.textContent = 'CRNs: ' + scheduleData[index].crns.join(', ');
    }
  };

  /**
   * Switch to schedule [[n + 1] % n] in the sequence
   */
  var next = function () {
    if (scheduleData.length > 0) {
      scheduleIndex = (++scheduleIndex < scheduleData.length ? scheduleIndex : 0);
      show(scheduleIndex);
    }
  }

  /**
   * Switch to schedule [[n - 1] % n] in the sequence
   */
  var previous = function () {
    if (scheduleData.length > 0) {
      scheduleIndex = (--scheduleIndex < 0 ? scheduleData.length - 1 : scheduleIndex);
      show(scheduleIndex);
    }
  }

  /**
   * Show next schedule if right is clicked or pressed,
   * show previous schedule if left is clicked or pressed
   */
  Yacs.on('click', leftSwitchElement, previous);
  Yacs.on('click', rightSwitchElement, next);
  Yacs.on('keydown', document, function (elem, event) { if (event.keyCode == 37) previous(); });
  Yacs.on('keydown', document, function (elem, event) { if (event.keyCode == 39) next(); });

  /**
   * Clear selections in cookie when clear button is pressed, and update
   * schedule and selctions views accordingly.
   */
  Yacs.on('click', clearButtonElement, function () {
    Yacs.user.clearSelections();
    updateSchedules();
    target.querySelectorAll('course-info').forEach(function (ci) {
      ci.classList.remove('selected');
    });
    target.querySelectorAll('section').forEach(function (s) {
      s.classList.remove('selected');
    });
  });

  /**
   * Show selected courses / sections on the schedule page.
   * Update schedules view when a course is clicked.
   * TODO: Use native event handling to update views when selection changes
   */
  var selections = Yacs.user.getSelections();
  if (selections.length > 0) {
    Yacs.models.courses.query({ section_id: selections.join(','),
                                show_sections: true,
                                show_periods: true },
      function (data, success) {
        if (success) {
          Yacs.views.courses(selectionElement, data);
          target.querySelectorAll('course').forEach(function (course) {
            Yacs.on('click', course, updateSchedules);
          });
        }
    });
  }

  updateSchedules();
};
<|endoftext|>


<commit_before>import config from '../../config';
import { deleteEntireWaysEpic, REQUEST_OSM_CHANGE_SUCCESS } from './osm';
import { editRoadIdEpic } from './roads';
/**
 * constants
 */
export const FETCH_WAY_TASK = 'FETCH_WAY_TASK';
export const FETCH_NEXT_WAY_TASK = 'FETCH_NEXT_WAY_TASK';
export const FETCH_WAY_TASK_SUCCESS = 'FETCH_WAY_TASK_SUCCESS';
export const FETCH_WAY_TASK_ERROR = 'FETCH_WAY_TASK_ERROR';
export const FETCH_WAY_TASK_COUNT = 'FETCH_WAY_TASK_COUNT';
export const FETCH_WAY_TASK_COUNT_SUCCESS = 'FETCH_WAY_TASK_COUNT_SUCCESS';
export const FETCH_WAY_TASK_COUNT_ERROR = 'FETCH_WAY_TASK_COUNT_ERROR';
export const MARK_WAY_TASK_PENDING = 'MARK_WAY_TASK_PENDING';
export const MARK_WAY_TASK_PENDING_SUCCESS = 'MARK_WAY_TASK_PENDING_SUCCESS';
export const MARK_WAY_TASK_PENDING_ERROR = 'MARK_WAY_TASK_PENDING_ERROR';
export const SKIP_TASK = 'SKIP_TASK';
export const SELECT_NEXT_WAY_TASK_PROVINCE = 'SELECT_NEXT_WAY_TASK_PROVINCE';
export const DEDUPE_WAY_TASK = 'DEDUPE_WAY_TASK';
export const DEDUPE_WAY_TASK_SUCCESS = 'DEDUPE_WAY_TASK_SUCCESS';
export const DEDUPE_WAY_TASK_ERROR = 'DEDUPE_WAY_TASK_ERROR';

/**
 * action creators
 */
export const fetchWayTask = (id) => ({ type: FETCH_WAY_TASK, id });
export const fetchNextWayTask = () => ({ type: FETCH_NEXT_WAY_TASK });
export const fetchWayTaskSuccess = (id, updatedAt, geoJSON) => ({
  type: FETCH_WAY_TASK_SUCCESS,
  id,
  updatedAt,
  geoJSON
});
export const fetchWayTaskError = (error) => ({ type: FETCH_WAY_TASK_ERROR, error: error });
export const fetchWayTaskCount = () => ({ type: FETCH_WAY_TASK_COUNT });
export const fetchWayTaskCountSuccess = count => ({ type: FETCH_WAY_TASK_COUNT_SUCCESS, count });
export const fetchWayTaskCountError = count => ({ type: FETCH_WAY_TASK_COUNT_ERROR });
export const markWayTaskPending = () => ({ type: MARK_WAY_TASK_PENDING });
export const markWayTaskPendingSuccess = () => ({ type: MARK_WAY_TASK_PENDING_SUCCESS });
export const markWayTaskPendingError = () => ({ type: MARK_WAY_TASK_PENDING_ERROR });
export const skipTask = id => ({ type: SKIP_TASK, id });
export const selectWayTaskProvince = (provinceId) => ({ type: SELECT_NEXT_WAY_TASK_PROVINCE, selectedProvince: provinceId });
export const dedupeWayTask = (taskId, wayIds, wayIdToKeep, dedupeId) => ({ type: DEDUPE_WAY_TASK, taskId: taskId, wayIdsToDelete: wayIds, wayIdToKeep: wayIdToKeep, dedupeIdToApply: dedupeId });
export const dedupeWayTaskSuccess = () => ({ type: DEDUPE_WAY_TASK_SUCCESS });
export const dedupeWayTaskError = (error) => ({ type: DEDUPE_WAY_TASK_ERROR, error: error });

export const fetchNextWayTaskEpic = () => (dispatch, getState) => {
  dispatch(fetchNextWayTask());

  const skipped = getState().waytasks.skipped;
  const selectedProvince = getState().waytasks.selectedProvince;

  let params = {};
  if (skipped.length) params['skip'] = skipped;
  if (selectedProvince) params['province'] = selectedProvince;

  let url = `${config.api}/tasks/next`;
  if (Object.keys(params).length) {
    url = url + '?' + Object.keys(params).map(key => `${key}=${params[key]}`).join('&');
  }

  return fetch(url)
    .then(response => {
      if (response.status === 404) {
        throw new Error('No tasks remaining');
      } else if (response.status >= 400) {
        throw new Error(response.statusText);
      }
      return response.json();
    })
    .then(json => {
      json.data.features.forEach(feature => {
        feature.properties._id = feature.meta.id;
        feature.properties.province = json.province;
      });
      dispatch(fetchWayTaskCountEpic());
      return dispatch(fetchWayTaskSuccess(json.id, json.updated_at, json.data));
    }, e => {
      console.error('Error requesting task', e);
      return dispatch(fetchWayTaskError(e));
    });
};

export const fetchWayTaskEpic = taskId => (dispatch, getState) => {
  dispatch(fetchWayTask(taskId));

  return fetch(`${config.api}/tasks/${taskId}`)
    .then(response => {
      if (response.status >= 400) {
        throw new Error(response.statusText);
      }
      return response.json();
    })
    .then(({ id, data }) => {
      data.features.forEach(feature => {
        feature.properties._id = feature.meta.id;
      });
      return dispatch(fetchWayTaskSuccess(id, data));
    }, e => {
      console.error('Error reloading task', e);
      return dispatch(fetchWayTaskError());
    });
};

export const fetchWayTaskCountEpic = () => (dispatch, getState) => {
  dispatch(fetchWayTaskCount());
  const selectedProvince = getState().waytasks.selectedProvince;
  const url = selectedProvince ? `${config.api}/tasks/count?province=${selectedProvince}` : `${config.api}/tasks/count`;

  fetch(url)
    .then(response => {
      if (response.status >= 400) {
        throw new Error(response.statusText);
      }
      return response.json();
    })
    .then(({ count }) => {
      dispatch(fetchWayTaskCountSuccess(count));
    }, e => {
      console.error('Error requesting task count', e);
      dispatch(fetchWayTaskCountError());
    });
};

export const markWayTaskPendingEpic = wayIds => dispatch => {
  dispatch(markWayTaskPending());

  return fetch(`${config.api}/tasks/pending`, {
    method: 'PUT',
    body: new Blob([JSON.stringify({ way_ids: wayIds })], { type: 'application/json' })
  })
    .then(response => {
      if (response.status >= 400) {
        console.error('Error marking task as pending', response.statusText);
        dispatch(markWayTaskPendingError());
      }

      dispatch(markWayTaskPendingSuccess());
    });
};

export const dedupeWayTaskEpic = (taskId, wayIds, wayIdToKeep, dedupeId) => (dispatch, getState) => {
  dispatch(dedupeWayTask(taskId, wayIds, wayIdToKeep, dedupeId));
  // delete ways
  dispatch(deleteEntireWaysEpic(taskId, wayIds))
    .then(() => {
      dispatch(editRoadIdEpic(wayIdToKeep, dedupeId))
        .then(() => { dispatch(dedupeWayTaskSuccess); })
        .catch((err) => { dispatch(dedupeWayTaskError(err)); });
    });
};
/**
 * reducer
 */
export default (
  state = {
    status: 'complete',
    countStatus: 'complete',
    id: null,
    geoJSON: null,
    skipped: [],
    provinces: [],
    selectedProvince: null,
    dedupeTaskId: null,
    wayIdsToDelete: [],
    dedupeIdToApply: null,
    wayIdToKeep: null,
    osmChangeTaskId: null
  },
  action
) => {
  if (action.type === FETCH_WAY_TASK) {
    return Object.assign({}, state, {
      status: 'pending'
    });
  } else if (action.type === FETCH_NEXT_WAY_TASK) {
    return Object.assign({}, state, {
      status: 'pending'
    });
  } else if (action.type === FETCH_WAY_TASK_SUCCESS) {
    return Object.assign({}, state, {
      geoJSON: action.geoJSON,
      id: action.id,
      updatedAt: action.updatedAt,
      status: 'complete'
    });
  } else if (action.type === FETCH_WAY_TASK_ERROR) {
    return Object.assign({}, state, {
      status: action.error.message === 'No tasks remaining' ? 'No tasks remaining' : 'error'
    });
  } else if (action.type === FETCH_WAY_TASK_COUNT) {
    return Object.assign({}, state, {
      taskCount: action.count,
      countStatus: 'pending'
    });
  } else if (action.type === FETCH_WAY_TASK_COUNT_SUCCESS) {
    return Object.assign({}, state, {
      taskCount: action.count,
      countStatus: 'complete'
    });
  } else if (action.type === FETCH_WAY_TASK_COUNT_ERROR) {
    return Object.assign({}, state, {
      countStatus: 'error'
    });
  } else if (action.type === SKIP_TASK) {
    return Object.assign({}, state, {
      skipped: state.skipped.concat(action.id)
    });
  } else if (action.type === SELECT_NEXT_WAY_TASK_PROVINCE) {
    return Object.assign({}, state, {
      selectedProvince: action.selectedProvince
    });
  } else if (action.type === DEDUPE_WAY_TASK) {
    return Object.assign({}, state, {
      dedupeTaskId: action.taskId,
      wayIdsToDelete: action.wayIdsToDelete,
      dedupeIdToApply: action.dedupeIdToApply,
      wayIdToKeep: action.wayIdToKeep
    });
  } else if (action.type === REQUEST_OSM_CHANGE_SUCCESS) {
    return Object.assign({}, state, {
      osmChangeTaskId: action.taskId
    });
  }

  return state;
};
<commit_msg>fix bug where panel shows blank post fixing a task due to bad order of parameters<commit_after>import config from '../../config';
import { deleteEntireWaysEpic, REQUEST_OSM_CHANGE_SUCCESS } from './osm';
import { editRoadIdEpic } from './roads';
/**
 * constants
 */
export const FETCH_WAY_TASK = 'FETCH_WAY_TASK';
export const FETCH_NEXT_WAY_TASK = 'FETCH_NEXT_WAY_TASK';
export const FETCH_WAY_TASK_SUCCESS = 'FETCH_WAY_TASK_SUCCESS';
export const FETCH_WAY_TASK_ERROR = 'FETCH_WAY_TASK_ERROR';
export const FETCH_WAY_TASK_COUNT = 'FETCH_WAY_TASK_COUNT';
export const FETCH_WAY_TASK_COUNT_SUCCESS = 'FETCH_WAY_TASK_COUNT_SUCCESS';
export const FETCH_WAY_TASK_COUNT_ERROR = 'FETCH_WAY_TASK_COUNT_ERROR';
export const MARK_WAY_TASK_PENDING = 'MARK_WAY_TASK_PENDING';
export const MARK_WAY_TASK_PENDING_SUCCESS = 'MARK_WAY_TASK_PENDING_SUCCESS';
export const MARK_WAY_TASK_PENDING_ERROR = 'MARK_WAY_TASK_PENDING_ERROR';
export const SKIP_TASK = 'SKIP_TASK';
export const SELECT_NEXT_WAY_TASK_PROVINCE = 'SELECT_NEXT_WAY_TASK_PROVINCE';
export const DEDUPE_WAY_TASK = 'DEDUPE_WAY_TASK';
export const DEDUPE_WAY_TASK_SUCCESS = 'DEDUPE_WAY_TASK_SUCCESS';
export const DEDUPE_WAY_TASK_ERROR = 'DEDUPE_WAY_TASK_ERROR';

/**
 * action creators
 */
export const fetchWayTask = (id) => ({ type: FETCH_WAY_TASK, id });
export const fetchNextWayTask = () => ({ type: FETCH_NEXT_WAY_TASK });
export const fetchWayTaskSuccess = (id, updatedAt, geoJSON) => ({
  type: FETCH_WAY_TASK_SUCCESS,
  id,
  updatedAt,
  geoJSON
});
export const fetchWayTaskError = (error) => ({ type: FETCH_WAY_TASK_ERROR, error: error });
export const fetchWayTaskCount = () => ({ type: FETCH_WAY_TASK_COUNT });
export const fetchWayTaskCountSuccess = count => ({ type: FETCH_WAY_TASK_COUNT_SUCCESS, count });
export const fetchWayTaskCountError = count => ({ type: FETCH_WAY_TASK_COUNT_ERROR });
export const markWayTaskPending = () => ({ type: MARK_WAY_TASK_PENDING });
export const markWayTaskPendingSuccess = () => ({ type: MARK_WAY_TASK_PENDING_SUCCESS });
export const markWayTaskPendingError = () => ({ type: MARK_WAY_TASK_PENDING_ERROR });
export const skipTask = id => ({ type: SKIP_TASK, id });
export const selectWayTaskProvince = (provinceId) => ({ type: SELECT_NEXT_WAY_TASK_PROVINCE, selectedProvince: provinceId });
export const dedupeWayTask = (taskId, wayIds, wayIdToKeep, dedupeId) => ({ type: DEDUPE_WAY_TASK, taskId: taskId, wayIdsToDelete: wayIds, wayIdToKeep: wayIdToKeep, dedupeIdToApply: dedupeId });
export const dedupeWayTaskSuccess = () => ({ type: DEDUPE_WAY_TASK_SUCCESS });
export const dedupeWayTaskError = (error) => ({ type: DEDUPE_WAY_TASK_ERROR, error: error });

export const fetchNextWayTaskEpic = () => (dispatch, getState) => {
  dispatch(fetchNextWayTask());

  const skipped = getState().waytasks.skipped;
  const selectedProvince = getState().waytasks.selectedProvince;

  let params = {};
  if (skipped.length) params['skip'] = skipped;
  if (selectedProvince) params['province'] = selectedProvince;

  let url = `${config.api}/tasks/next`;
  if (Object.keys(params).length) {
    url = url + '?' + Object.keys(params).map(key => `${key}=${params[key]}`).join('&');
  }

  return fetch(url)
    .then(response => {
      if (response.status === 404) {
        throw new Error('No tasks remaining');
      } else if (response.status >= 400) {
        throw new Error(response.statusText);
      }
      return response.json();
    })
    .then(json => {
      json.data.features.forEach(feature => {
        feature.properties._id = feature.meta.id;
        feature.properties.province = json.province;
      });
      dispatch(fetchWayTaskCountEpic());
      return dispatch(fetchWayTaskSuccess(json.id, json.updated_at, json.data));
    }, e => {
      console.error('Error requesting task', e);
      return dispatch(fetchWayTaskError(e));
    });
};

export const fetchWayTaskEpic = taskId => (dispatch, getState) => {
  dispatch(fetchWayTask(taskId));

  return fetch(`${config.api}/tasks/${taskId}`)
    .then(response => {
      if (response.status >= 400) {
        throw new Error(response.statusText);
      }
      return response.json();
    })
    .then(({ id, data }) => {
      data.features.forEach(feature => {
        feature.properties._id = feature.meta.id;
      });
      return dispatch(fetchWayTaskSuccess(id, data.updated_at, data));
    }, e => {
      console.error('Error reloading task', e);
      return dispatch(fetchWayTaskError());
    });
};

export const fetchWayTaskCountEpic = () => (dispatch, getState) => {
  dispatch(fetchWayTaskCount());
  const selectedProvince = getState().waytasks.selectedProvince;
  const url = selectedProvince ? `${config.api}/tasks/count?province=${selectedProvince}` : `${config.api}/tasks/count`;

  fetch(url)
    .then(response => {
      if (response.status >= 400) {
        throw new Error(response.statusText);
      }
      return response.json();
    })
    .then(({ count }) => {
      dispatch(fetchWayTaskCountSuccess(count));
    }, e => {
      console.error('Error requesting task count', e);
      dispatch(fetchWayTaskCountError());
    });
};

export const markWayTaskPendingEpic = wayIds => dispatch => {
  dispatch(markWayTaskPending());

  return fetch(`${config.api}/tasks/pending`, {
    method: 'PUT',
    body: new Blob([JSON.stringify({ way_ids: wayIds })], { type: 'application/json' })
  })
    .then(response => {
      if (response.status >= 400) {
        console.error('Error marking task as pending', response.statusText);
        dispatch(markWayTaskPendingError());
      }

      dispatch(markWayTaskPendingSuccess());
    });
};

export const dedupeWayTaskEpic = (taskId, wayIds, wayIdToKeep, dedupeId) => (dispatch, getState) => {
  dispatch(dedupeWayTask(taskId, wayIds, wayIdToKeep, dedupeId));
  // delete ways
  dispatch(deleteEntireWaysEpic(taskId, wayIds))
    .then(() => {
      dispatch(editRoadIdEpic(wayIdToKeep, dedupeId))
        .then(() => { dispatch(dedupeWayTaskSuccess); })
        .catch((err) => { dispatch(dedupeWayTaskError(err)); });
    });
};
/**
 * reducer
 */
export default (
  state = {
    status: 'complete',
    countStatus: 'complete',
    id: null,
    geoJSON: null,
    skipped: [],
    provinces: [],
    selectedProvince: null,
    dedupeTaskId: null,
    wayIdsToDelete: [],
    dedupeIdToApply: null,
    wayIdToKeep: null,
    osmChangeTaskId: null
  },
  action
) => {
  if (action.type === FETCH_WAY_TASK) {
    return Object.assign({}, state, {
      status: 'pending'
    });
  } else if (action.type === FETCH_NEXT_WAY_TASK) {
    return Object.assign({}, state, {
      status: 'pending'
    });
  } else if (action.type === FETCH_WAY_TASK_SUCCESS) {
    return Object.assign({}, state, {
      geoJSON: action.geoJSON,
      id: action.id,
      updatedAt: action.updatedAt,
      status: 'complete'
    });
  } else if (action.type === FETCH_WAY_TASK_ERROR) {
    return Object.assign({}, state, {
      status: action.error.message === 'No tasks remaining' ? 'No tasks remaining' : 'error'
    });
  } else if (action.type === FETCH_WAY_TASK_COUNT) {
    return Object.assign({}, state, {
      taskCount: action.count,
      countStatus: 'pending'
    });
  } else if (action.type === FETCH_WAY_TASK_COUNT_SUCCESS) {
    return Object.assign({}, state, {
      taskCount: action.count,
      countStatus: 'complete'
    });
  } else if (action.type === FETCH_WAY_TASK_COUNT_ERROR) {
    return Object.assign({}, state, {
      countStatus: 'error'
    });
  } else if (action.type === SKIP_TASK) {
    return Object.assign({}, state, {
      skipped: state.skipped.concat(action.id)
    });
  } else if (action.type === SELECT_NEXT_WAY_TASK_PROVINCE) {
    return Object.assign({}, state, {
      selectedProvince: action.selectedProvince
    });
  } else if (action.type === DEDUPE_WAY_TASK) {
    return Object.assign({}, state, {
      dedupeTaskId: action.taskId,
      wayIdsToDelete: action.wayIdsToDelete,
      dedupeIdToApply: action.dedupeIdToApply,
      wayIdToKeep: action.wayIdToKeep
    });
  } else if (action.type === REQUEST_OSM_CHANGE_SUCCESS) {
    return Object.assign({}, state, {
      osmChangeTaskId: action.taskId
    });
  }

  return state;
};
<|endoftext|>


<commit_before>JobResults = new Mongo.Collection('jobResults');

JobResults.schema = new SimpleSchema({
  jobId: {
    type: String,
    regEx: SimpleSchema.RegEx.Id,
    index: true
  },
  parameters: {
    type: Object,
    blackbox: true
  },
  runId: {
    type: String,
    regEx: SimpleSchema.RegEx.Id
  },
  status: {
    type: String,
    allowedValues: ['ok', 'running', 'error']
  },
  data: SimpleSchema.oneOf({
    type: String,
    optional: true
  }, {
    type: Array,
  }),
  'data.$': {
    type: Object,
    blackbox: true
  },
  fields: {
    type: Array,
    defaultValue: [],
  },
  'fields.$': String,
  updatedAt: {
    type: Date,
    autoValue: () => new Date()
  },
  expiresAt: {
    type: Date
  },
  pid: {
    type: Number,
    optional: true
  }
});

JobResults.attachSchema(JobResults.schema);<commit_msg>only change updatedAt for job results if it actually materially changes (ok, error)<commit_after>JobResults = new Mongo.Collection('jobResults');

JobResults.schema = new SimpleSchema({
  jobId: {
    type: String,
    regEx: SimpleSchema.RegEx.Id,
    index: true
  },
  parameters: {
    type: Object,
    blackbox: true
  },
  runId: {
    type: String,
    regEx: SimpleSchema.RegEx.Id
  },
  status: {
    type: String,
    allowedValues: ['ok', 'running', 'error']
  },
  data: SimpleSchema.oneOf({
    type: String,
    optional: true
  }, {
    type: Array,
  }),
  'data.$': {
    type: Object,
    blackbox: true
  },
  fields: {
    type: Array,
    defaultValue: [],
  },
  'fields.$': String,
  updatedAt: {
    type: Date,
    autoValue: function() {
      const status = this.field('status').value;
      if (['ok', 'error'].indexOf(status) !== -1 || this.isInsert)
        return new Date();
      else if (this.isUpsert)
        return { $setOnInsert: new Date() };
    }
  },
  expiresAt: {
    type: Date
  },
  pid: {
    type: Number,
    optional: true
  }
});

JobResults.attachSchema(JobResults.schema);<|endoftext|>