<commit_before>function ToolbarButton(options, remotePontoon, remoteLinks) {
    this._options = options;
    this._remotePontoon = remotePontoon;
    this._remoteLinks = remoteLinks;
    this._defaultTitle = 'Pontoon notifications';
    this._updateError = false;
    this._refreshInterval;

    this._init();
}

ToolbarButton.prototype = {
    _init: function() {
        this._addOnClickAction();
        this._addContextMenu();
        this._watchStorageChanges();
        this._watchOptionsUpdates();
        this._listenToMessagesFromPopup();
        this._reload();
    },

    _updateNumberOfUnreadNotifications: function() {
        this._updateBadge('');
        this._remotePontoon.updateNotificationsDocText();
    },

    _setRefresh: function(intervalMinutes) {
        clearInterval(this._refreshInterval);
        this._refreshInterval = setInterval(this._updateNumberOfUnreadNotifications.bind(this), intervalMinutes * 60 * 1000);
    },

    _scheduleOrUpdateRefreshWithInterval: function(intervalMinutes) {
        if (!this._updateError) {
            this._setRefresh(intervalMinutes);
        } else {
            this._setRefresh(1);
        }
    },

    _scheduleOrUpdateRefresh: function() {
        var optionKey = 'options.notifications_update_interval';
        this._options.get([optionKey], function(item) {
            var intervalMinutes = parseInt(item[optionKey], 10);
            this._scheduleOrUpdateRefreshWithInterval(intervalMinutes);
        }.bind(this));
    },

    _watchStorageChanges: function() {
        chrome.storage.onChanged.addListener(function(changes, areaName) {
            var docKey = 'notificationsDocText';
            if (changes[docKey] !== undefined) {
                var docContent = changes[docKey].newValue;
                if (docContent != undefined) {
                    var notificationsDoc = new DOMParser().parseFromString(docContent, 'text/html');
                    var unreadCount = notificationsDoc.querySelectorAll('header .notification-item[data-unread=true]').length;
                    this._updateBadge(unreadCount);
                } else {
                    this._updateBadge('!');
                    this._scheduleOrUpdateRefresh();
                }
            }
        }.bind(this));
    },

    _watchOptionsUpdates: function() {
        chrome.storage.onChanged.addListener(function(changes, areaName) {
            var updateIntervalOptionKey = 'options.notifications_update_interval';
            if (changes[updateIntervalOptionKey] !== undefined) {
                var intervalMinutes = parseInt(changes[updateIntervalOptionKey].newValue, 10);
                this._scheduleOrUpdateRefreshWithInterval(intervalMinutes);
            }
            var openPontoonOptionKey = 'options.open_pontoon_on_button_click';
            if (changes[openPontoonOptionKey]) {
                this._setPopup(!changes[openPontoonOptionKey].newValue);
            }
        }.bind(this));
    },

    _listenToMessagesFromPopup: function() {
        chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
            if (request.type == 'notifications-reload-request') {
                this._reload();
            }
        }.bind(this));
    },

    _setPopup: function(showPopup) {
        if (showPopup) {
            chrome.browserAction.setPopup({popup: chrome.extension.getURL('html/notifications-popup.html')});
        } else {
            chrome.browserAction.setPopup({popup: ''});
        }
    },

    _addOnClickAction: function() {
        chrome.browserAction.onClicked.addListener(function(tab) {
            chrome.tabs.create({url: this._remotePontoon.getTeamPageUrl()});
        }.bind(this));
        var optionKey = 'options.open_pontoon_on_button_click';
        this._options.get([optionKey], function(item) {
            this._setPopup(!item[optionKey]);
        }.bind(this));
    },

    _addContextMenu: function() {
        chrome.contextMenus.create({
            title: 'Reload notifications',
            contexts: ['browser_action'],
            onclick: this._reload.bind(this),
        });
        var pontoonPagesMenuId = chrome.contextMenus.create({
            title: 'Pontoon pages',
            contexts: ['browser_action'],
        });
        chrome.contextMenus.create({
            title: 'Home',
            contexts: ['browser_action'],
            parentId: pontoonPagesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remotePontoon.getBaseUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'Team page',
            contexts: ['browser_action'],
            parentId: pontoonPagesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remotePontoon.getTeamPageUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'Team bugs',
            contexts: ['browser_action'],
            parentId: pontoonPagesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remotePontoon.getTeamBugsUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'Machinery',
            contexts: ['browser_action'],
            parentId: pontoonPagesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remotePontoon.getMachineryUrl()});}.bind(this),
        });
        var localizationResourcesMenuId = chrome.contextMenus.create({
            title: 'Other l10n sources',
            contexts: ['browser_action'],
        });
        chrome.contextMenus.create({
            title: 'Transvision',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getTransvisionUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'amaGama.locamotion.org',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getAmaGamaUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'Microsoft Terminology Search',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getMicrosoftTerminologySearchUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            type: 'separator',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
        });
        chrome.contextMenus.create({
            title: 'Cambridge Dictionary',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getCambridgeDictionaryUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            type: 'separator',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
        });
        chrome.contextMenus.create({
            title: 'Mozilla Style Guides',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getMozillaStyleGuidesUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            type: 'separator',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
        });
        chrome.contextMenus.create({
            title: 'Mozilla l10n Team dashboard',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getElmoDashboardUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'Mozilla Web l10n Dashboard',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getWebDashboardUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'MozillaWiki L10n Team',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getMozillaWikiL10nTeamUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'Pontoon Tools options',
            contexts: ['browser_action'],
            onclick: function() {chrome.runtime.openOptionsPage();}.bind(this),
        });
    },

    _updateBadge: function(text) {
        chrome.browserAction.setBadgeText({text: text.toString()});
        if (text.toString().trim().length == 0) {
            chrome.browserAction.setTitle({title: this._defaultTitle});
        } else {
            chrome.browserAction.setTitle({title: `${this._defaultTitle} (${text.toString()})`});
        }
        if (text != 0) {
            chrome.browserAction.setBadgeBackgroundColor({color: '#F36'});
        } else {
            chrome.browserAction.setBadgeBackgroundColor({color: '#4d5967'});
        }
    },

    _reload: function() {
        this._updateNumberOfUnreadNotifications();
        this._scheduleOrUpdateRefresh();
    },
}
<commit_msg>Mark all notifications as read from the button context menu<commit_after>function ToolbarButton(options, remotePontoon, remoteLinks) {
    this._options = options;
    this._remotePontoon = remotePontoon;
    this._remoteLinks = remoteLinks;
    this._defaultTitle = 'Pontoon notifications';
    this._updateError = false;
    this._refreshInterval;

    this._init();
}

ToolbarButton.prototype = {
    _init: function() {
        this._addOnClickAction();
        this._addContextMenu();
        this._watchStorageChanges();
        this._watchOptionsUpdates();
        this._listenToMessagesFromPopup();
        this._reload();
    },

    _updateNumberOfUnreadNotifications: function() {
        this._updateBadge('');
        this._remotePontoon.updateNotificationsDocText();
    },

    _setRefresh: function(intervalMinutes) {
        clearInterval(this._refreshInterval);
        this._refreshInterval = setInterval(this._updateNumberOfUnreadNotifications.bind(this), intervalMinutes * 60 * 1000);
    },

    _scheduleOrUpdateRefreshWithInterval: function(intervalMinutes) {
        if (!this._updateError) {
            this._setRefresh(intervalMinutes);
        } else {
            this._setRefresh(1);
        }
    },

    _scheduleOrUpdateRefresh: function() {
        var optionKey = 'options.notifications_update_interval';
        this._options.get([optionKey], function(item) {
            var intervalMinutes = parseInt(item[optionKey], 10);
            this._scheduleOrUpdateRefreshWithInterval(intervalMinutes);
        }.bind(this));
    },

    _watchStorageChanges: function() {
        chrome.storage.onChanged.addListener(function(changes, areaName) {
            var docKey = 'notificationsDocText';
            if (changes[docKey] !== undefined) {
                var docContent = changes[docKey].newValue;
                if (docContent != undefined) {
                    var notificationsDoc = new DOMParser().parseFromString(docContent, 'text/html');
                    var unreadCount = notificationsDoc.querySelectorAll('header .notification-item[data-unread=true]').length;
                    this._updateBadge(unreadCount);
                } else {
                    this._updateBadge('!');
                    this._scheduleOrUpdateRefresh();
                }
            }
        }.bind(this));
    },

    _watchOptionsUpdates: function() {
        chrome.storage.onChanged.addListener(function(changes, areaName) {
            var updateIntervalOptionKey = 'options.notifications_update_interval';
            if (changes[updateIntervalOptionKey] !== undefined) {
                var intervalMinutes = parseInt(changes[updateIntervalOptionKey].newValue, 10);
                this._scheduleOrUpdateRefreshWithInterval(intervalMinutes);
            }
            var openPontoonOptionKey = 'options.open_pontoon_on_button_click';
            if (changes[openPontoonOptionKey]) {
                this._setPopup(!changes[openPontoonOptionKey].newValue);
            }
        }.bind(this));
    },

    _listenToMessagesFromPopup: function() {
        chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
            if (request.type == 'notifications-reload-request') {
                this._reload();
            }
        }.bind(this));
    },

    _setPopup: function(showPopup) {
        if (showPopup) {
            chrome.browserAction.setPopup({popup: chrome.extension.getURL('html/notifications-popup.html')});
        } else {
            chrome.browserAction.setPopup({popup: ''});
        }
    },

    _addOnClickAction: function() {
        chrome.browserAction.onClicked.addListener(function(tab) {
            chrome.tabs.create({url: this._remotePontoon.getTeamPageUrl()});
        }.bind(this));
        var optionKey = 'options.open_pontoon_on_button_click';
        this._options.get([optionKey], function(item) {
            this._setPopup(!item[optionKey]);
        }.bind(this));
    },

    _addContextMenu: function() {
        chrome.contextMenus.create({
            title: 'Mark all Notifications as read',
            contexts: ['browser_action'],
            onclick: this._remotePontoon.markAllNotificationsAsRead.bind(this._remotePontoon),
        });
        chrome.contextMenus.create({
            title: 'Reload notifications',
            contexts: ['browser_action'],
            onclick: this._reload.bind(this),
        });
        var pontoonPagesMenuId = chrome.contextMenus.create({
            title: 'Pontoon pages',
            contexts: ['browser_action'],
        });
        chrome.contextMenus.create({
            title: 'Home',
            contexts: ['browser_action'],
            parentId: pontoonPagesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remotePontoon.getBaseUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'Team page',
            contexts: ['browser_action'],
            parentId: pontoonPagesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remotePontoon.getTeamPageUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'Team bugs',
            contexts: ['browser_action'],
            parentId: pontoonPagesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remotePontoon.getTeamBugsUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'Machinery',
            contexts: ['browser_action'],
            parentId: pontoonPagesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remotePontoon.getMachineryUrl()});}.bind(this),
        });
        var localizationResourcesMenuId = chrome.contextMenus.create({
            title: 'Other l10n sources',
            contexts: ['browser_action'],
        });
        chrome.contextMenus.create({
            title: 'Transvision',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getTransvisionUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'amaGama.locamotion.org',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getAmaGamaUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'Microsoft Terminology Search',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getMicrosoftTerminologySearchUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            type: 'separator',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
        });
        chrome.contextMenus.create({
            title: 'Cambridge Dictionary',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getCambridgeDictionaryUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            type: 'separator',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
        });
        chrome.contextMenus.create({
            title: 'Mozilla Style Guides',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getMozillaStyleGuidesUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            type: 'separator',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
        });
        chrome.contextMenus.create({
            title: 'Mozilla l10n Team dashboard',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getElmoDashboardUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'Mozilla Web l10n Dashboard',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getWebDashboardUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'MozillaWiki L10n Team',
            contexts: ['browser_action'],
            parentId: localizationResourcesMenuId,
            onclick: function() {chrome.tabs.create({url: this._remoteLinks.getMozillaWikiL10nTeamUrl()});}.bind(this),
        });
        chrome.contextMenus.create({
            title: 'Pontoon Tools options',
            contexts: ['browser_action'],
            onclick: function() {chrome.runtime.openOptionsPage();}.bind(this),
        });
    },

    _updateBadge: function(text) {
        chrome.browserAction.setBadgeText({text: text.toString()});
        if (text.toString().trim().length == 0) {
            chrome.browserAction.setTitle({title: this._defaultTitle});
        } else {
            chrome.browserAction.setTitle({title: `${this._defaultTitle} (${text.toString()})`});
        }
        if (text != 0) {
            chrome.browserAction.setBadgeBackgroundColor({color: '#F36'});
        } else {
            chrome.browserAction.setBadgeBackgroundColor({color: '#4d5967'});
        }
    },

    _reload: function() {
        this._updateNumberOfUnreadNotifications();
        this._scheduleOrUpdateRefresh();
    },
}
<|endoftext|>


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

// MAIN PAGE ELEMENTS DIRECTIVES

.directive('navigationBar', function() {
    return {
        restrict: 'A',
        templateUrl: 'navigation.xhtml',
        controller: 'dialogController'
    };
})

.directive('footerBar', function() {
    return {
        restrict: 'A',
        templateUrl: 'footer.xhtml'
    };
})

.directive("toggleFilter", function() {
    return {
        restrict: "A",
        link: function() {
            $("#nav-filter").click(function() {
                $("#recipes-bar").slideToggle("fast");
            });
        }
    };
})

// RECIPE CARD/POPUP DIRECTIVES

.directive('recipeBook', function() {
    return {
        restrict: 'A',
        templateUrl: 'recipeTml.xhtml',
        link: function($scope, element, attrs) {
            $scope.$watch('filterName', function(newVal, oldVal) {
                //filter Json
                if (newVal !== oldVal) {
                    $scope.recipesSix = diveRecipesIntoRowAndFilter($scope.recipes, newVal, $scope.nameSort);
                }
            });

            $scope.$watch('nameSort', function(newVal, oldVal) {
                //filter Json
                if (newVal !== oldVal) {
                    $scope.recipesSix = diveRecipesIntoRowAndFilter($scope.recipes, $scope.filterName, newVal);
                }
            });
        }
    };
})

.directive('recipeCard', function() {
    return {
        restrict: 'E',
        templateUrl: 'recipeCardTml.xhtml',
        controller: 'dialogController',
        scope: {
            recipe: '=',
            addRecipeCard: '='
        }
    };
})

.directive('recipeImg', function() {
    return {
        restrict: 'E',
        template: '',
        scope: {
            recipe: '='
        }
    };
})

.directive('recipeTitle', function() {
    return {
        restrict: 'A',
        template: '{{recipe.name}}',
        scope: {
            recipe: '='
        }
    };
})

.directive('recipeShortDescription', function() {
    return {
        restrict: 'A',
        template: '{{ truncateWithEllipsis(recipe.description, 50) }}',
        controller: 'dialogController',
        scope: {
            recipe: '='
        }
    };
})

.directive('recipeDescription', function() {
    return {
        restrict: 'A',
        template: '{{ recipe.description }}',
        scope: {
            recipe: '='
        }
    };
})

.directive('recipeIngredient', function() {
    return {
        restrict: 'A',
        template: ' {{ingredient}}',
        scope: {
            ingredient: '='
        }
    };
})

// SIGN IN/UP POPUP DIRECTIVES

.directive("compareTo", function() {
    return {
        require: "ngModel",
        scope: {
            otherModelValue: "=compareTo"
        },
        link: function(scope, element, attributes, ngModel) {

            ngModel.$validators.compareTo = function(modelValue) {
                return modelValue == scope.otherModelValue;
            };

            scope.$watch("otherModelValue", function() {
                ngModel.$validate();
            });
        }
    };
})
<commit_msg>CC Issue fix.<commit_after>angular.module('appDirectives', [])

// MAIN PAGE ELEMENTS DIRECTIVES

.directive('navigationBar', function() {
    return {
        restrict: 'A',
        templateUrl: 'navigation.xhtml',
        controller: 'dialogController'
    };
})

.directive('footerBar', function() {
    return {
        restrict: 'A',
        templateUrl: 'footer.xhtml'
    };
})

.directive("toggleFilter", function() {
    return {
        restrict: "A",
        link: function() {
            $("#nav-filter").click(function() {
                $("#recipes-bar").slideToggle("fast");
            });
        }
    };
})

// RECIPE CARD/POPUP DIRECTIVES

.directive('recipeBook', function() {
    return {
        restrict: 'A',
        templateUrl: 'recipeTml.xhtml',
        link: function($scope, element, attrs) {
            $scope.$watch('filterName', function(newVal, oldVal) {
                //filter Json
                if (newVal !== oldVal) {
                    $scope.recipesSix = diveRecipesIntoRowAndFilter($scope.recipes, newVal, $scope.nameSort);
                }
            });

            $scope.$watch('nameSort', function(newVal, oldVal) {
                //filter Json
                if (newVal !== oldVal) {
                    $scope.recipesSix = diveRecipesIntoRowAndFilter($scope.recipes, $scope.filterName, newVal);
                }
            });
        }
    };
})

.directive('recipeCard', function() {
    return {
        restrict: 'E',
        templateUrl: 'recipeCardTml.xhtml',
        controller: 'dialogController',
        scope: {
            recipe: '=',
            addRecipeCard: '='
        }
    };
})

.directive('recipeImg', function() {
    return {
        restrict: 'E',
        template: '',
        scope: {
            recipe: '='
        }
    };
})

.directive('recipeTitle', function() {
    return {
        restrict: 'A',
        template: '{{recipe.name}}',
        scope: {
            recipe: '='
        }
    };
})

.directive('recipeShortDescription', function() {
    return {
        restrict: 'A',
        template: '{{ truncateWithEllipsis(recipe.description, 50) }}',
        controller: 'dialogController',
        scope: {
            recipe: '='
        }
    };
})

.directive('recipeDescription', function() {
    return {
        restrict: 'A',
        template: '{{ recipe.description }}',
        scope: {
            recipe: '='
        }
    };
})

.directive('recipeIngredient', function() {
    return {
        restrict: 'A',
        template: ' {{ingredient}}',
        scope: {
            ingredient: '='
        }
    };
})

// SIGN IN/UP POPUP DIRECTIVES

.directive("compareTo", function() {
    return {
        require: "ngModel",
        scope: {
            otherModelValue: "=compareTo"
        },
        link: function(scope, element, attributes, ngModel) {

            ngModel.$validators.compareTo = function(modelValue) {
                return modelValue === otherModelValue;
            };

            scope.$watch("otherModelValue", function() {
                ngModel.$validate();
            });
        }
    };
})
<|endoftext|>


<commit_before>// Strider Simple Worker
// Niall O'Higgins 2012
//
// A simple, in-process worker implementation for Strider.
//

var async = require('async')
var exec = require('child_process').exec
var EventEmitter = require('events').EventEmitter
var gitane = require('gitane')
var gumshoe = require('gumshoe')
var path = require('path')
var spawn = require('child_process').spawn
var Step = require('step')

// Work around npm not being installed on some systems - use own copy
// the test clause is for Heroku
var npmCmd = "$(test -x ~/bin/node && echo ~/bin/node || echo node) ../../node_modules/npm/bin/npm-cli.js"
var nodePrepare = npmCmd + " install"
var nodeTest = npmCmd + " test"
var nodeStart = npmCmd + " start"

// Built-in rules for project-type detection
var DEFAULT_PROJECT_TYPE_RULES = [
  // Node
  {filename:"package.json", grep:/express/i, language:"node.js", framework:"express", prepare:nodePrepare, test:nodeTest, start:nodeStart},
  {filename:"package.json", grep:/connect/i, language:"node.js", framework:"connect", prepare:nodePrepare, test:nodeTest, start:nodeStart},
  {filename:"package.json", exists:true, language:"node.js", framework:null, prepare:nodePrepare, test:nodeTest, start:nodeStart},
]

// detection rules which may be added by worker plugins
// rules must contain a property *test* which can either be a string or a function taking a callback.
// if a string, this is a shell command to be executed to run project tests (e.g. "npm install")
// if a function, this accepts a callback argument of signature function(err) which must be called.
//
// rules may contain a property *prepare* which can be a string or a function like *test*. this is for pre-test
// preparations (e.g. "npm install")
//
var detectionRules = []
// pre-start commands which may be added by worker plugins
// E.g. to start mongodb or postgresql
// pre-start commands may be either a string or a function
// XXX: foreground vs daemonprocesses

// if a string, this is a shell command to be executed to run project tests (e.g. "mongod")
// if a function, this accepts a callback argument of signature function(err) which must be called.
var setupActions = []
// teardown commands which may be added by worker plugins
// E.g. to stop mongodb or postgresql
// teardown commands may be either a string or a function
// XXX: foreground vs daemonprocesses
// if a string, this is a shell command to be executed to run project tests (e.g. "kill $PID")
// if a function, this accepts a callback argument of signature function(err) which must be called.
var teardownActions = []

// Return path to writeable dir for test purposes
function getDataDir() {
  return path.join(__dirname, "_work")
}

// Wrap a shell command for execution by spawn()
function shellWrap(str) {
  return { cmd:"sh", args:["-c", str] }
}

// Default logger
logger = {log: console.log}

function registerEvents(emitter) {

  // Use an async.queue of concurrency 1 to ensure jobs are processed serially
  var q = async.queue(function(task, cb) {
    processJob(task, cb)
  }, 1)

  //
  // the queue.new_job event is primary way jobs are submitted
  //
  emitter.on('queue.new_job', function(data) {
    q.push(data)
  })

  function processJob(data, done) {
    // cross-process (per-job) output buffers
    var stderrBuffer = ""
    var stdoutBuffer = ""
    var stdmergedBuffer = ""
    // Put stuff under `_work`
    var dir = getDataDir()
    logger.log('new job')
    // Start the clock
    var t1 = new Date()

    // Emit a status update event. This can result in data being sent to the
    // user's browser in realtime via socket.io.
    function updateStatus(evType, opts) {
      var t2 = new Date()
      var elapsed = (t2.getTime() - t1.getTime()) / 1000
      var msg = {
        userId:data.user_id,
        jobId:data.job_id,
        timeElapsed:elapsed,
        repoUrl:data.repo_config.url,
        stdout: opts.stdout || "",
        stderr: opts.stderr || "",
        stdmerged: opts.stdmerged || "",
        autodetectResult:opts.autodetectResult || null,
        testExitCode: null,
        deployExitCode: null,
      }
      if (opts.testExitCode !== undefined) {
        msg.testExitCode = opts.testExitCode
      }
      if (opts.deployExitCode !== undefined) {
        msg.deployExitCode = opts.deployExitCode
      }

      emitter.emit(evType, msg)
    }

    // Insert a synthetic (non job-generated) output message
    // This automatically prefixes with "[STRIDER]" to make the source
    // of the message clearer to the user.
    function striderMessage(message) {
      var msg = "[STRIDER] " + message + "\n"
      stdmergedBuffer += msg
      stdoutBuffer += msg
      updateStatus("queue.job_update", {stdout:msg, stdmerged:msg})
    }

    // Deploy to Heroku
    function deployHeroku(cwd, app, key, cb) {
      var cmd = 'git remote add heroku git@heroku.com:' + app + '.git'
      gitane.run(cwd, key, cmd, function(err, stdout, stderr) {
        if (err) return cb(1, null)
        stdoutBuffer += stdout
        stderrBuffer += stderr
        stdmergedBuffer += stdout + stderr
        updateStatus("queue.job_update", {stdout:stdout, stderr:stderr, stdmerged:stdout+stderr})
        cmd = 'git push heroku --force master'
        gitane.run(cwd, key, cmd, function(err, stdout, stderr) {
          if (err) return cb(1, null)
          stdoutBuffer += stdout
          stderrBuffer += stderr
          stdmergedBuffer += stdout + stderr
          updateStatus("queue.job_update", {stdout:stdout, stderr:stderr, stdmerged:stdout+stderr})
          striderMessage("Deployment to Heroku successful.")
          cb(0)
        })
      })
    }

    function mergeObj(src, dst) {
      var keys = Object.keys(src)
      for (var i=0; i < keys.length; i++) {
        dst[keys[i]] = src[keys[i]]
      }
    }

    //
    // forkProc(cwd, shell, cb)
    // or
    // forkProc(cwd, cmd, args, cb)
    // or
    // forkProc({opts}, cb)
    //
    function forkProc(cwd, cmd, args, cb) {
      var env = process.env
      if (data.repo_config.env !== undefined) {
        mergeObj(data.repo_config.env, env)
      }
      if (typeof(cwd) === 'object') {
        cb = cmd
        var cmd = cwd.cmd
        var args = cwd.args
        // Merge/override any variables
        mergeObj(cwd.env, env)
        cwd = cwd.cwd
      }
      if (typeof(cmd) === 'string' && typeof(args) === 'function') {
        var split = cmd.split(/\s+/)
        cmd = split[0]
        cb = args
        args = split.slice(1)
      }
      env.PAAS_NAME = 'strider'
      var proc = spawn(cmd, args, {cwd: cwd, env: env})

      // per-process output buffers
      proc.stderrBuffer = ""
      proc.stdoutBuffer = ""
      proc.stdmergedBuffer = ""

      proc.stdout.setEncoding('utf8')
      proc.stderr.setEncoding('utf8')

      proc.stdout.on('data', function(buf) {
        proc.stdoutBuffer += buf
        proc.stdmergedBuffer += buf
        stdoutBuffer += buf
        stdmergedBuffer += buf
        updateStatus("queue.job_update" , {stdout:buf})
      })

      proc.stderr.on('data', function(buf) {
        proc.stderrBuffer += buf
        proc.stdmergedBuffer += buf
        stderrBuffer += buf
        stdmergedBuffer += buf
        updateStatus("queue.job_update", {stderr:buf})
      })

      proc.on('close', function(exitCode) {
        logger.log("process exited with code: %d", exitCode)
        cb(exitCode)
      })

      return proc
    }

    Step(
      function() {
        // XXX: Support incremental builds at some point
        exec('rm -rf ' + dir + ' ; mkdir -p ' + dir, this)
      },
      function(err) {
        if (err) throw err
        logger.log("cloning %s into %s", data.repo_ssh_url, dir)
        var msg = "Starting git clone of repo at " + data.repo_ssh_url
        striderMessage(msg)
        gitane.run(dir, data.repo_config.privkey, 'git clone --recursive ' + data.repo_ssh_url, this)
      },
      function(err, stderr, stdout) {
        if (err) throw err
        this.workingDir = path.join(dir, path.basename(data.repo_ssh_url.replace('.git', '')))
        updateStatus("queue.job_update", {stdout:stdout, stderr:stderr, stdmerged:stdout+stderr})
        var msg = "Git clone complete"
        logger.log(msg)
        striderMessage(msg)
        gumshoe.run(this.workingDir, detectionRules, this)
      },
      function(err, result) {
        if (err) throw err
        // TODO: Setup phase (database bringup, etc)

        // Context object for action functions
        var context = {
          forkProc: forkProc,
          updateStatus: updateStatus,
          striderMessage: striderMessage,
          shellWrap:shellWrap,
          workingDir: this.workingDir,
          jobData: data,
          npmCmd: npmCmd,
          events: new EventEmitter(),
        }

        // No-op defaults
        var prepare, test, deploy
        prepare = test = deploy = function(context, cb) {
          cb(null)
        }
        // If this job has a Heroku deploy config attached, use the Heroku deploy function
        if (data.deploy_config) {
          logger.log("have heroku config")
          var self = this
          deploy = function(ctx, cb) {
            striderMessage("Deploying to Heroku ...")
            deployHeroku(self.workingDir,
              data.deploy_config.app, data.deploy_config.privkey, cb)
          }
        }

        function complete(testCode, deployCode, cb) {
          updateStatus("queue.job_complete", {
            stderr:stderrBuffer,
            stdout:stdoutBuffer,
            stdmerged:stdmergedBuffer,
            testExitCode:testCode,
            deployExitCode:deployCode
          })
          if (typeof(cb) === 'function') cb(null)
        }

        // If actions are strings, we assume they are shell commands and try to execute them
        // directly ourselves.
        var self = this
        if (typeof(result.prepare) === 'string') {
          var psh = shellWrap(result.prepare)
          prepare = function(context, cb) {
            forkProc(self.workingDir, psh.cmd, psh.args, cb)
          }
        }

        if (typeof(result.test) === 'string') {
          var tsh = shellWrap(result.test)
          test = function(context, cb) {
            forkProc(self.workingDir, tsh.cmd, tsh.args, cb)
          }
        }

        if (typeof(result.deploy) === 'string') {
          var tsh = shellWrap(result.deploy)
          deploy = function(context, cb) {
            forkProc(self.workingDir, tsh.cmd, tsh.args, cb)
          }
        }
        // Execution actions may be delegated to functions.
        // This is useful for example for multi-step things like in Python where a virtual env must be set up.
        // Functions are of signature function(context, cb)
        // We assume the function handles any necessary shell interaction and sending of update messages.
        if (typeof(result.prepare) === 'function') {
          prepare = result.prepare
        }
        if (typeof(result.test) === 'function') {
          test = result.test
        }
        if (typeof(result.deploy) === 'function') {
          deploy = result.deploy
        }

        prepare(context, function(prepareExitCode) {
          if (prepareExitCode !== 0) {
            // Prepare step failed
            var msg = "Prepare failed; exit code: " + prepareExitCode
            striderMessage(msg)
            logger.log(msg)
            logger.log("stdmergedBuffer: %s", stdmergedBuffer);
            return complete(prepareExitCode, null, done)
          }
          test(context, function(testExitCode) {
            var msg = "Test exit code: " + testExitCode
            logger.log(msg)
            striderMessage(msg)
            if (testExitCode !== 0 || data.job_type !== "TEST_AND_DEPLOY") {
              // Test step failed or no deploy requested
              complete(testExitCode, null, done)
            } else {
              // Test step passed and deploy requested
              deploy(context, function(deployExitCode) {
                complete(testExitCode, deployExitCode, done)
              })
            }

          })
        })

        // TODO: Teardown phase (database shutdown, etc)

      }

    )
  }
}

// Add an array of detection rules to head of list
function addDetectionRules(r) {
  detectionRules = r.concat(detectionRules)
}

// Add a single detection rule to head of list
function addDetectionRule(r) {
  detectionRules = [r].concat(detectionRules)
}

module.exports = function(context, cb) {
  // XXX test purposes
  detectionRules = DEFAULT_PROJECT_TYPE_RULES
  // Build a worker context, which is a stripped-down version of the webapp context
  var workerContext = {
    addDetectionRule:addDetectionRule,
    addDetectionRules:addDetectionRules,
    config: context.config,
    extdir: context.extdir,
    npmCmd: npmCmd,
  }

  // Hooks for tests
  if (context.gitane) {
    gitane = context.gitane
  }
  if (context.gumshoe) {
    gumshoe = context.gumshoe
  }
  if (context.exec) {
    exec = context.exec
  }
  if (context.log) {
    logger = {log: context.log}
  }

  Step(
    function() {
      context.loader.initExtensions(context.extdir, "worker", workerContext, null, this)
    },
    function(err, initialized) {
      registerEvents(context.emitter)
      logger.log("Strider Simple Worker ready")
      cb(null, null)
    }
  )
}
<commit_msg>Don't reclone repo every time.<commit_after>// Strider Simple Worker
// Niall O'Higgins 2012
//
// A simple, in-process worker implementation for Strider.
//

var async = require('async')
var exec = require('child_process').exec
var EventEmitter = require('events').EventEmitter
var gitane = require('gitane')
var gumshoe = require('gumshoe')
var path = require('path')
var spawn = require('child_process').spawn
var Step = require('step')
var fs = require('fs')

// Work around npm not being installed on some systems - use own copy
// the test clause is for Heroku
var npmCmd = "$(test -x ~/bin/node && echo ~/bin/node || echo node) ../../node_modules/npm/bin/npm-cli.js"
var nodePrepare = npmCmd + " install"
var nodeTest = npmCmd + " test"
var nodeStart = npmCmd + " start"

// Built-in rules for project-type detection
var DEFAULT_PROJECT_TYPE_RULES = [
  // Node
  {filename:"package.json", grep:/express/i, language:"node.js", framework:"express", prepare:nodePrepare, test:nodeTest, start:nodeStart},
  {filename:"package.json", grep:/connect/i, language:"node.js", framework:"connect", prepare:nodePrepare, test:nodeTest, start:nodeStart},
  {filename:"package.json", exists:true, language:"node.js", framework:null, prepare:nodePrepare, test:nodeTest, start:nodeStart},
]

// detection rules which may be added by worker plugins
// rules must contain a property *test* which can either be a string or a function taking a callback.
// if a string, this is a shell command to be executed to run project tests (e.g. "npm install")
// if a function, this accepts a callback argument of signature function(err) which must be called.
//
// rules may contain a property *prepare* which can be a string or a function like *test*. this is for pre-test
// preparations (e.g. "npm install")
//
var detectionRules = []
// pre-start commands which may be added by worker plugins
// E.g. to start mongodb or postgresql
// pre-start commands may be either a string or a function
// XXX: foreground vs daemonprocesses

// if a string, this is a shell command to be executed to run project tests (e.g. "mongod")
// if a function, this accepts a callback argument of signature function(err) which must be called.
var setupActions = []
// teardown commands which may be added by worker plugins
// E.g. to stop mongodb or postgresql
// teardown commands may be either a string or a function
// XXX: foreground vs daemonprocesses
// if a string, this is a shell command to be executed to run project tests (e.g. "kill $PID")
// if a function, this accepts a callback argument of signature function(err) which must be called.
var teardownActions = []

// Return path to writeable dir for test purposes
function getDataDir() {
  return path.join(__dirname, "_work")
}

// Wrap a shell command for execution by spawn()
function shellWrap(str) {
  return { cmd:"sh", args:["-c", str] }
}

// Default logger
logger = {log: console.log}

function registerEvents(emitter) {

  // Use an async.queue of concurrency 1 to ensure jobs are processed serially
  var q = async.queue(function(task, cb) {
    processJob(task, cb)
  }, 1)

  //
  // the queue.new_job event is primary way jobs are submitted
  //
  emitter.on('queue.new_job', function(data) {
    q.push(data)
  })

  function processJob(data, done) {
    // cross-process (per-job) output buffers
    var stderrBuffer = ""
    var stdoutBuffer = ""
    var stdmergedBuffer = ""
    // Put stuff under `_work`
    var dir = getDataDir()
    logger.log('new job')
    // Start the clock
    var t1 = new Date()

    // Emit a status update event. This can result in data being sent to the
    // user's browser in realtime via socket.io.
    function updateStatus(evType, opts) {
      var t2 = new Date()
      var elapsed = (t2.getTime() - t1.getTime()) / 1000
      var msg = {
        userId:data.user_id,
        jobId:data.job_id,
        timeElapsed:elapsed,
        repoUrl:data.repo_config.url,
        stdout: opts.stdout || "",
        stderr: opts.stderr || "",
        stdmerged: opts.stdmerged || "",
        autodetectResult:opts.autodetectResult || null,
        testExitCode: null,
        deployExitCode: null,
      }
      if (opts.testExitCode !== undefined) {
        msg.testExitCode = opts.testExitCode
      }
      if (opts.deployExitCode !== undefined) {
        msg.deployExitCode = opts.deployExitCode
      }

      emitter.emit(evType, msg)
    }

    // Insert a synthetic (non job-generated) output message
    // This automatically prefixes with "[STRIDER]" to make the source
    // of the message clearer to the user.
    function striderMessage(message) {
      var msg = "[STRIDER] " + message + "\n"
      stdmergedBuffer += msg
      stdoutBuffer += msg
      updateStatus("queue.job_update", {stdout:msg, stdmerged:msg})
    }

    // Deploy to Heroku
    function deployHeroku(cwd, app, key, cb) {
      var cmd = 'git remote add heroku git@heroku.com:' + app + '.git'
      gitane.run(cwd, key, cmd, function(err, stdout, stderr) {
        if (err) return cb(1, null)
        stdoutBuffer += stdout
        stderrBuffer += stderr
        stdmergedBuffer += stdout + stderr
        updateStatus("queue.job_update", {stdout:stdout, stderr:stderr, stdmerged:stdout+stderr})
        cmd = 'git push heroku --force master'
        gitane.run(cwd, key, cmd, function(err, stdout, stderr) {
          if (err) return cb(1, null)
          stdoutBuffer += stdout
          stderrBuffer += stderr
          stdmergedBuffer += stdout + stderr
          updateStatus("queue.job_update", {stdout:stdout, stderr:stderr, stdmerged:stdout+stderr})
          striderMessage("Deployment to Heroku successful.")
          cb(0)
        })
      })
    }

    function mergeObj(src, dst) {
      var keys = Object.keys(src)
      for (var i=0; i < keys.length; i++) {
        dst[keys[i]] = src[keys[i]]
      }
    }

    //
    // forkProc(cwd, shell, cb)
    // or
    // forkProc(cwd, cmd, args, cb)
    // or
    // forkProc({opts}, cb)
    //
    function forkProc(cwd, cmd, args, cb) {
      var env = process.env
      if (data.repo_config.env !== undefined) {
        mergeObj(data.repo_config.env, env)
      }
      if (typeof(cwd) === 'object') {
        cb = cmd
        var cmd = cwd.cmd
        var args = cwd.args
        // Merge/override any variables
        mergeObj(cwd.env, env)
        cwd = cwd.cwd
      }
      if (typeof(cmd) === 'string' && typeof(args) === 'function') {
        var split = cmd.split(/\s+/)
        cmd = split[0]
        cb = args
        args = split.slice(1)
      }
      env.PAAS_NAME = 'strider'
      var proc = spawn(cmd, args, {cwd: cwd, env: env})

      // per-process output buffers
      proc.stderrBuffer = ""
      proc.stdoutBuffer = ""
      proc.stdmergedBuffer = ""

      proc.stdout.setEncoding('utf8')
      proc.stderr.setEncoding('utf8')

      proc.stdout.on('data', function(buf) {
        proc.stdoutBuffer += buf
        proc.stdmergedBuffer += buf
        stdoutBuffer += buf
        stdmergedBuffer += buf
        updateStatus("queue.job_update" , {stdout:buf})
      })

      proc.stderr.on('data', function(buf) {
        proc.stderrBuffer += buf
        proc.stdmergedBuffer += buf
        stderrBuffer += buf
        stdmergedBuffer += buf
        updateStatus("queue.job_update", {stderr:buf})
      })

      proc.on('close', function(exitCode) {
        logger.log("process exited with code: %d", exitCode)
        cb(exitCode)
      })

      return proc
    }

    Step(
      function() {
        var next = this;
        // Check if there's a git repo or not:
        var workingDir = path.join(dir, path.basename(data.repo_ssh_url.replace('.git', '')))
        console.log(workingDir);
        if (fs.existsSync(workingDir + '/.git')){
          // Assume that the repo is good and that there are no
          // local-only commits.
          // TODO: Maybe fix this?
          // TODO: This assumes there will never be another repo with the same name :( would be better to clone into dir named after ssh_url
          var msg = "Updating repo from " + data.repo_ssh_url
          striderMessage(msg)
          gitane.run(workingDir, data.repo_config.privkey, 'git reset --hard', function(err){
            if(err) throw err;
            gitane.run(workingDir, data.repo_config.privkey, 'git pull', next);
          })
        } else {
          exec('rm -rf ' + dir + ' ; mkdir -p ' + dir, function(err){
            if (err) throw err
            logger.log("cloning %s into %s", data.repo_ssh_url, dir)
            var msg = "Starting git clone of repo at " + data.repo_ssh_url
            striderMessage(msg)
            gitane.run(dir, data.repo_config.privkey, 'git clone --recursive ' + data.repo_ssh_url, next)
          })
        }
      },
      function(err, stderr, stdout) {
        if (err) throw err
        this.workingDir = path.join(dir, path.basename(data.repo_ssh_url.replace('.git', '')))
        updateStatus("queue.job_update", {stdout:stdout, stderr:stderr, stdmerged:stdout+stderr})
        var msg = "Git clone complete"
        logger.log(msg)
        striderMessage(msg)
        gumshoe.run(this.workingDir, detectionRules, this)
      },
      function(err, result) {
        if (err) throw err
        // TODO: Setup phase (database bringup, etc)

        // Context object for action functions
        var context = {
          forkProc: forkProc,
          updateStatus: updateStatus,
          striderMessage: striderMessage,
          shellWrap:shellWrap,
          workingDir: this.workingDir,
          jobData: data,
          npmCmd: npmCmd,
          events: new EventEmitter(),
        }

        // No-op defaults
        var prepare, test, deploy
        prepare = test = deploy = function(context, cb) {
          cb(null)
        }
        // If this job has a Heroku deploy config attached, use the Heroku deploy function
        if (data.deploy_config) {
          logger.log("have heroku config")
          var self = this
          deploy = function(ctx, cb) {
            striderMessage("Deploying to Heroku ...")
            deployHeroku(self.workingDir,
              data.deploy_config.app, data.deploy_config.privkey, cb)
          }
        }

        function complete(testCode, deployCode, cb) {
          updateStatus("queue.job_complete", {
            stderr:stderrBuffer,
            stdout:stdoutBuffer,
            stdmerged:stdmergedBuffer,
            testExitCode:testCode,
            deployExitCode:deployCode
          })
          if (typeof(cb) === 'function') cb(null)
        }

        // If actions are strings, we assume they are shell commands and try to execute them
        // directly ourselves.
        var self = this
        if (typeof(result.prepare) === 'string') {
          var psh = shellWrap(result.prepare)
          prepare = function(context, cb) {
            forkProc(self.workingDir, psh.cmd, psh.args, cb)
          }
        }

        if (typeof(result.test) === 'string') {
          var tsh = shellWrap(result.test)
          test = function(context, cb) {
            forkProc(self.workingDir, tsh.cmd, tsh.args, cb)
          }
        }

        if (typeof(result.deploy) === 'string') {
          var tsh = shellWrap(result.deploy)
          deploy = function(context, cb) {
            forkProc(self.workingDir, tsh.cmd, tsh.args, cb)
          }
        }
        // Execution actions may be delegated to functions.
        // This is useful for example for multi-step things like in Python where a virtual env must be set up.
        // Functions are of signature function(context, cb)
        // We assume the function handles any necessary shell interaction and sending of update messages.
        if (typeof(result.prepare) === 'function') {
          prepare = result.prepare
        }
        if (typeof(result.test) === 'function') {
          test = result.test
        }
        if (typeof(result.deploy) === 'function') {
          deploy = result.deploy
        }

        prepare(context, function(prepareExitCode) {
          if (prepareExitCode !== 0) {
            // Prepare step failed
            var msg = "Prepare failed; exit code: " + prepareExitCode
            striderMessage(msg)
            logger.log(msg)
            logger.log("stdmergedBuffer: %s", stdmergedBuffer);
            return complete(prepareExitCode, null, done)
          }
          test(context, function(testExitCode) {
            var msg = "Test exit code: " + testExitCode
            logger.log(msg)
            striderMessage(msg)
            if (testExitCode !== 0 || data.job_type !== "TEST_AND_DEPLOY") {
              // Test step failed or no deploy requested
              complete(testExitCode, null, done)
            } else {
              // Test step passed and deploy requested
              deploy(context, function(deployExitCode) {
                complete(testExitCode, deployExitCode, done)
              })
            }

          })
        })

        // TODO: Teardown phase (database shutdown, etc)

      }

    )
  }
}

// Add an array of detection rules to head of list
function addDetectionRules(r) {
  detectionRules = r.concat(detectionRules)
}

// Add a single detection rule to head of list
function addDetectionRule(r) {
  detectionRules = [r].concat(detectionRules)
}

module.exports = function(context, cb) {
  // XXX test purposes
  detectionRules = DEFAULT_PROJECT_TYPE_RULES
  // Build a worker context, which is a stripped-down version of the webapp context
  var workerContext = {
    addDetectionRule:addDetectionRule,
    addDetectionRules:addDetectionRules,
    config: context.config,
    extdir: context.extdir,
    npmCmd: npmCmd,
  }

  // Hooks for tests
  if (context.gitane) {
    gitane = context.gitane
  }
  if (context.gumshoe) {
    gumshoe = context.gumshoe
  }
  if (context.exec) {
    exec = context.exec
  }
  if (context.log) {
    logger = {log: context.log}
  }

  Step(
    function() {
      context.loader.initExtensions(context.extdir, "worker", workerContext, null, this)
    },
    function(err, initialized) {
      registerEvents(context.emitter)
      logger.log("Strider Simple Worker ready")
      cb(null, null)
    }
  )
}
<|endoftext|>


<commit_before>(function() {
    "use strict";

    if (!SirTrevor)
        return console.error("SirTrevor.Blocks.Button could not load because SirTrevor wasn't found");

    SirTrevor.Blocks.Button = SirTrevor.Block.extend({
        type: 'button',
        title: function() { return i18n.t('blocks:button:title'); },
        icon_name: 'button',

        clonable: true,

        editorHTML: function() {
            return _.template('@@include("button/editor.html")', { imports: { i18n: i18n } });
        },

        onBlockRender: function() {
            // Setup shortcuts
            this.$preview = this.$el.find('.st-preview');
            this.$css = this.$editor.find('[name^="css-"]');

            // setup spectrum
            this.$editor.find('[type="color"]').spectrum({
                showInput: true,
                preferredFormat: "hex",
                showButtons: true,
                chooseText: i18n.t('blocks:button:accept'),
                cancelText: i18n.t('blocks:button:cancel')
            });

            // Set the default button text
            if ($(this.getTextBlockHTML()).text().length <= 0)
                this.setTextBlockHTML(i18n.t("blocks:button:hint:text"));

            this.$editor.find('[name="user-href"]')
                .attr('placeholder', i18n.t("blocks:button:hint:href"))
                .on('change input', this._onHrefChange.bind(this));// Listen for suer href changes and update the actual href

            // Listen for css inputs changes to refresh the preview
            this.$css.on('change input', this._onCssPropertyChange.bind(this));
            // Listen for spectrum color changes (as they click on the color pallete)
            this.$css.filter('[name*="color"]').on('move.spectrum', this._onCssPropertyChange.bind(this));
            // Refresh the preview
            this.$css.trigger('change');
        },

        loadData: function(data) {
            this.setTextBlockHTML(data.text);
            Object.keys(data)
            .forEach(function (key) {
                var val = data[key];

                if (key.indexOf('color') >= 0 && val.indexOf('rgb') >= 0) // colors: rgb -> hex
                    val = this._rgbToHex(val);

                var $ell = this.$el.find('[name="'+ key + '"]');

                if ($ell.attr('units') && $ell.attr('units').length > 0)
                    val = val.replace($ell.attr('units'), '');

                this.$el.find('[name="'+ key + '"]').val(val);
            }.bind(this))
            var $preview = this.$el.find('.st-preview');
            var $rows = this.$el.find('.st-row');
            $rows.hide();
            $preview.attr('contenteditable', 'false');
            $preview.click(function() {
                $rows.show();
                $preview.attr('contenteditable', 'true');
                $preview.unbind('click');
            });
        },

        _serializeData: function() {
            var data = {};

            if (this.hasTextBlock()) {
                data.text = this.getTextBlockHTML();
                data.format = 'html';
            }

            this.$('input, select').each(function(index, input){
                if (input.getAttribute('name')) {
                    var val = input.value;
                    if (input.getAttribute('units'))
                        val += input.getAttribute('units');
                    data[input.getAttribute('name')] = val;
                }
            });

            return data;
        },

        _onHrefChange: function(ev) {
            var $source = $(ev.target);
            var value = $source.val();
            var $target = this.$editor.find('[name="href"]');

            // ref: http://www.regular-expressions.info/email.html
            if (/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value))
                $target.val('mailto:' + value);
            else if (/^\+?[0-9\-]+$/.test(value))
                $target.val('tel:' + value);
            else if (/^(ftp|http|https):\/\/[^ "]+$/.test(value))
                $target.val(value);
            else
                $target.val('http://' + value);
        },

        _onCssPropertyChange: function (ev, value) {
            var $target = $(ev.target);
            // Get the css property from the name
            var props = $target.attr('name').replace(/^css\-/, '');
            var val = value ? value.toString() : $target.val();

            // This is used for number indicator in range input
            $target.attr('st-value', val);

            if (value)
                $target.val(value);

            if ($target.attr('type') == 'range') {
                var _after = $target.next();
                _after.css('content', val);
            }

            if ($target.attr('units') && $target.attr('units').length > 0)
                val += $target.attr('units');

            props.split('_').forEach(function (prop) {
                this.$preview.css(prop, val);
            }, this);
        },

        _rgbToHex: function(color) {
            color = String(color).trim();

            if (color.indexOf('rgb(') != 0 || color.charAt(color.length - 1) != ')') // Dont even know what is this, do nothing
                return color;

            var rgbVals = color
                            .replace(/^rgb\(/, '')
                            .replace(/\)$/, '')
                            .split(/[\s,]+/)
                            .filter(function(n) { return !isNaN(n) })

            if (rgbVals.length != 3) // Don't know what is this, return the original silently
                return color;

            return rgbVals.reduce(function(c, val) {
                var hex = Number(val).toString(16);
                if (hex.length == 1)
                    hex = '0' + hex
                return c + hex
            }, '#')
        }
    })
})();<commit_msg>Blocks/Button: If href is empty, dont apend anything to it<commit_after>(function() {
    "use strict";

    if (!SirTrevor)
        return console.error("SirTrevor.Blocks.Button could not load because SirTrevor wasn't found");

    SirTrevor.Blocks.Button = SirTrevor.Block.extend({
        type: 'button',
        title: function() { return i18n.t('blocks:button:title'); },
        icon_name: 'button',

        clonable: true,

        editorHTML: function() {
            return _.template('@@include("button/editor.html")', { imports: { i18n: i18n } });
        },

        onBlockRender: function() {
            // Setup shortcuts
            this.$preview = this.$el.find('.st-preview');
            this.$css = this.$editor.find('[name^="css-"]');

            // setup spectrum
            this.$editor.find('[type="color"]').spectrum({
                showInput: true,
                preferredFormat: "hex",
                showButtons: true,
                chooseText: i18n.t('blocks:button:accept'),
                cancelText: i18n.t('blocks:button:cancel')
            });

            // Set the default button text
            if ($(this.getTextBlockHTML()).text().length <= 0)
                this.setTextBlockHTML(i18n.t("blocks:button:hint:text"));

            this.$editor.find('[name="user-href"]')
                .attr('placeholder', i18n.t("blocks:button:hint:href"))
                .on('change input', this._onHrefChange.bind(this));// Listen for suer href changes and update the actual href

            // Listen for css inputs changes to refresh the preview
            this.$css.on('change input', this._onCssPropertyChange.bind(this));
            // Listen for spectrum color changes (as they click on the color pallete)
            this.$css.filter('[name*="color"]').on('move.spectrum', this._onCssPropertyChange.bind(this));
            // Refresh the preview
            this.$css.trigger('change');
        },

        loadData: function(data) {
            this.setTextBlockHTML(data.text);
            Object.keys(data)
            .forEach(function (key) {
                var val = data[key];

                if (key.indexOf('color') >= 0 && val.indexOf('rgb') >= 0) // colors: rgb -> hex
                    val = this._rgbToHex(val);

                var $ell = this.$el.find('[name="'+ key + '"]');

                if ($ell.attr('units') && $ell.attr('units').length > 0)
                    val = val.replace($ell.attr('units'), '');

                this.$el.find('[name="'+ key + '"]').val(val);
            }.bind(this))
            var $preview = this.$el.find('.st-preview');
            var $rows = this.$el.find('.st-row');
            $rows.hide();
            $preview.attr('contenteditable', 'false');
            $preview.click(function() {
                $rows.show();
                $preview.attr('contenteditable', 'true');
                $preview.unbind('click');
            });
        },

        _serializeData: function() {
            var data = {};

            if (this.hasTextBlock()) {
                data.text = this.getTextBlockHTML();
                data.format = 'html';
            }

            this.$('input, select').each(function(index, input){
                if (input.getAttribute('name')) {
                    var val = input.value;
                    if (input.getAttribute('units'))
                        val += input.getAttribute('units');
                    data[input.getAttribute('name')] = val;
                }
            });

            return data;
        },

        _onHrefChange: function(ev) {
            var $source = $(ev.target);
            var value = $source.val();
            var $target = this.$editor.find('[name="href"]');

            // ref: http://www.regular-expressions.info/email.html
            if (/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value))
                $target.val('mailto:' + value);
            else if (/^\+?[0-9\-]+$/.test(value))
                $target.val('tel:' + value);
            else if (/^(ftp|http|https):\/\/[^ "]+$/.test(value))
                $target.val(value);
            else if (value.length > 0)
                $target.val('http://' + value);
            else
                $target.val(value);
        },

        _onCssPropertyChange: function (ev, value) {
            var $target = $(ev.target);
            // Get the css property from the name
            var props = $target.attr('name').replace(/^css\-/, '');
            var val = value ? value.toString() : $target.val();

            // This is used for number indicator in range input
            $target.attr('st-value', val);

            if (value)
                $target.val(value);

            if ($target.attr('type') == 'range') {
                var _after = $target.next();
                _after.css('content', val);
            }

            if ($target.attr('units') && $target.attr('units').length > 0)
                val += $target.attr('units');

            props.split('_').forEach(function (prop) {
                this.$preview.css(prop, val);
            }, this);
        },

        _rgbToHex: function(color) {
            color = String(color).trim();

            if (color.indexOf('rgb(') != 0 || color.charAt(color.length - 1) != ')') // Dont even know what is this, do nothing
                return color;

            var rgbVals = color
                            .replace(/^rgb\(/, '')
                            .replace(/\)$/, '')
                            .split(/[\s,]+/)
                            .filter(function(n) { return !isNaN(n) })

            if (rgbVals.length != 3) // Don't know what is this, return the original silently
                return color;

            return rgbVals.reduce(function(c, val) {
                var hex = Number(val).toString(16);
                if (hex.length == 1)
                    hex = '0' + hex
                return c + hex
            }, '#')
        }
    })
})();<|endoftext|>


<commit_before>'use strict';
var request = require('request');
var fs = require('fs');

module.exports = {
  config: {},
  routes: function (app, context) {
    app.get('/report', function (req, res) {
      var org = req.params.org;
      var repo = req.params.repo;
      var branch = req.query.branch;
      var protocol = req.protocol;
      var hostName = req.get('Host');
      var project = org + '/' + repo;
      var coveragLocation = '~/.strider/data/'+ org + '-' + repo + '-' + branch + '/*/coverage/index.html';
      res.render(coveragLocation);
      // res.json({yes: app});
      // var jobsEndpoint = protocol + '://' + hostName + '/' + org + '/' + repo + '/jobs';
      // console.log('BODY', body);
      // var latestJobId = JSON.parse(body)[0];
    });
  },
  globalRoutes: function (app, context) {
    app.get('/report', function (req, res) {
      res.json({context: context});
    });
  }
};
<commit_msg>reports endpoint<commit_after>'use strict';
var request = require('request');
var fs = require('fs');

module.exports = {
  config: {},
  routes: function (app, context) {
    app.get('/report', function (req, res) {
      var org = req.params.org;
      var repo = req.params.repo;
      var branch = req.query.branch;
      var protocol = req.protocol;
      var hostName = req.get('Host');
      var project = org + '/' + repo;
      var coveragLocation = '~/.strider/data/'+ org + '-' + repo + '-' + branch + '/*/coverage/index.html';
      fs.readFile(coveragLocation, 'utf8', function (err, data) {
        console.log(data, err)
        res.render(data);
      });
      // res.json({yes: app});
      // var jobsEndpoint = protocol + '://' + hostName + '/' + org + '/' + repo + '/jobs';
      // console.log('BODY', body);
      // var latestJobId = JSON.parse(body)[0];
    });
  },
  globalRoutes: function (app, context) {
    app.get('/report', function (req, res) {
      res.json({context: context});
    });
  }
};
<|endoftext|>


<commit_before>// Add persistent bindings between a selector and a model value.  Changes are propagated between the two.
//
// To use this, you should add have an `options.bindings` element to your component.  It should look something like:
//
// bindings: {
//   "key": {
//     selector: "selector1",
//     path:     "path1",
//   },
//   "selector2": "path2"
// }
//
// There are two ways of specifying bindings.  The "long form" has named keys (as in the first example above) and
// supports the following options:
//
// * selector:    A valid selector for your component.
//                Must be able to be resolved using that.locate(selector)
//
// * path:        A valid path for the model variable whose value will be watched.
//                Must be able to be resolved using fluid.get(path)
//
// The "short form" uses the selector as the key, and the path as a string value (as in the second example above).
//
// To make use of either form, you will need to start by sourcing this file after all fluid sources.
//
// Then, in your component, you will need a valid selector, as in:
//
//  selectors: {
//      "input":    ".ptd-search-input"
//  }
//
// You will also need a valid model path, as in:
//
//  model: {
//      input:  ""
//  }
//
// Finally, you will need to invoke the binding applier where it makes sense, for example in response to an event.
// Here's an example that binds to the "onCreate" event:
//
//  listeners: {
//      onCreate: {
//        "funcName": "gpii.templates.binder.applyBinding",
//        "args":     "{that}"
//      }
//  }
//
// Once you have done all this, model changes should be passed to form controls and vice versa.
//
// Note that if you reload markup or otherwise generate new markup, you will need to reapply the set of bindings.
// The `templateAware` grade included with this package takes care of that for you by calling `fluid.initDomBinder`
// and then running `applyBinding` once the `onDomBind` event is fired.  See that component for a working example.
//
// This code was originally written by Dr. Antranig Basman <amb26@ponder.org.uk> and with his advice was updated and
// extended by Tony Atkins <tony@raisingthefloor.org>.
//
/* global fluid, jQuery */
(function ($) {
    "use strict";
    fluid.registerNamespace("gpii.templates.binder");

    // The main function to create bindings between markup and model elements.  See above for usage details.
    gpii.templates.binder.applyBinding = function (that) {
        var bindings = that.options.bindings;
        fluid.each(bindings, function (value, key) {
            var path     = typeof value === "string" ? value : value.path;
            var selector = typeof value === "string" ? key : value.selector;
            var type     = value.type ? value.type : "text";
            var element = that.locate(selector);

            // Update the model when the form changes
            element.change(function () {
                fluid.log("Changing model based on element update.");

                var elementValue = fluid.value(element);
                that.applier.change(path, elementValue);
            });

            // Update the form elements when the model changes
            that.applier.modelChanged.addListener(path, function (change) {
                fluid.log("Changing value based on model update.");

                // We must use a separate method to update radio buttons
                if (type === "radio") {
                    element.val(change[path]);
                    element.each(function (index, button) {
                        if (button.value === change[path]) {
                            button.checked = true;
                        }
                    });
                }
                else {
                    fluid.value(element, change[path]);
                }
            });

            // If we have model data initially, update the form.  Model values win out over markup.
            var initialModelValue = fluid.get(that.model, path);
            if (initialModelValue) {
                fluid.value(element, initialModelValue);
            }
            // If we have no model data, but there are defaults in the markup, using them to update the model.
            else {
                var initialFormValue = fluid.value(element);
                if (initialFormValue) {
                    that.applier.change(path, initialFormValue);
                }
            }
        });
    };
})(jQuery);

<commit_msg>GPII-1175:  Updated binder documentation based on pull request feedback.<commit_after>// Add persistent bindings between a selector and a model value.  Changes are propagated between the two.
//
// To use this, you should add have an `options.bindings` element to your component.  It should look something like:
//
// bindings: {
//   "key": {
//     selector: "selector1",
//     path:     "path1",
//     type:     "radio"
//   },
//   "selector2": "path2"
// }
//
// There are two ways of specifying bindings.  The "long form" has named keys (as in the first example above) and
// supports the following options:
//
// * selector:    A valid selector for your component.
//                Must be able to be resolved using that.locate(selector)
//
// * path:        A valid path for the model variable whose value will be watched.
//                Must be able to be resolved using fluid.get(path)
//
// * type:        The type of form input we are dealing with.  Only needed when dealing with `radio` inputs, which
//                Require special handling.
//
// The "short form" uses the selector as the key, and the path as a string value (as in the second example above).
//
// To make use of either form, you will need to start by sourcing this file after all fluid sources.
//
// Then, in your component, you will need a valid selector, as in:
//
//  selectors: {
//      "input":    ".ptd-search-input"
//  }
//
// You will also need a valid model path, as in:
//
//  model: {
//      input:  ""
//  }
//
// Finally, you will need to invoke the binding applier where it makes sense, for example in response to an event.
// Here's an example that binds to the "onCreate" event:
//
//  listeners: {
//      onCreate: {
//        "funcName": "gpii.templates.binder.applyBinding",
//        "args":     "{that}"
//      }
//  }
//
// Once you have done all this, model changes should be passed to form controls and vice versa.
//
// Note that if you reload markup or otherwise generate new markup, you will need to reapply the set of bindings.
// The `templateAware` grade included with this package takes care of that for you by calling `fluid.initDomBinder`
// and then running `applyBinding` once the `onDomBind` event is fired.  See that component for a working example.
//
// This code was originally written by Antranig Basman <amb26@ponder.org.uk> and with his advice was updated and
// extended by Tony Atkins <tony@raisingthefloor.org>.
//
/* global fluid, jQuery */
(function () {
    "use strict";
    var gpii = fluid.registerNamespace("gpii");
    fluid.registerNamespace("gpii.templates.binder");

    // The main function to create bindings between markup and model elements.  See above for usage details.
    gpii.templates.binder.applyBinding = function (that) {
        var bindings = that.options.bindings;
        fluid.each(bindings, function (value, key) {
            var path     = typeof value === "string" ? value : value.path;
            var selector = typeof value === "string" ? key : value.selector;
            var type     = value.type ? value.type : "text";
            var element = that.locate(selector);

            // Update the model when the form changes
            element.change(function () {
                fluid.log("Changing model based on element update.");

                var elementValue = fluid.value(element);
                that.applier.change(path, elementValue);
            });

            // Update the form elements when the model changes
            that.applier.modelChanged.addListener(path, function (change) {
                fluid.log("Changing value based on model update.");

                // We must use a separate method to update radio buttons
                if (type === "radio") {
                    element.val(change[path]);
                    element.each(function (index, button) {
                        if (button.value === change[path]) {
                            button.checked = true;
                        }
                    });
                }
                else {
                    fluid.value(element, change[path]);
                }
            });

            // If we have model data initially, update the form.  Model values win out over markup.
            var initialModelValue = fluid.get(that.model, path);
            if (initialModelValue) {
                fluid.value(element, initialModelValue);
            }
            // If we have no model data, but there are defaults in the markup, using them to update the model.
            else {
                var initialFormValue = fluid.value(element);
                if (initialFormValue) {
                    that.applier.change(path, initialFormValue);
                }
            }
        });
    };
})(jQuery);

<|endoftext|>


<commit_before>/*
* NOTE: THE VARIABLE BELOW NEEDS A MASHAPE IDENTIFIER TO WORK.
* If you want to use this code in your own Transloc bus app, you need your own identifier
* To get a key, make an account at https://www.mashape.com/ and find your environment keys there.
*/
var identifier = "";
var agency = "283"; // This is the agency id for University of Rochester. If using another agency, change this variable to that id.
/*
 * Function to get the arrival time estimate for the given route stop combination.
 */
function getTimeEstimate(route, stop) {
		// If either the route or the stop are empty, then simply return -1 (no arrival estimate.)
		if (route == 0 || stop == 0) {
			Pebble.sendAppMessage({
				"CURRENT_VIEW_TIME": -1
			});
			return;
		}
		// Get in touch with the transloc service for the info.
		var req = new XMLHttpRequest();
		req.open('GET', 'https://transloc-api-1-2.p.mashape.com/arrival-estimates.json?agencies=' + agency + '&routes=' + route + '&stops=' + stop);
		req.setRequestHeader("X-Mashape-Authorization", identifier);
		req.onload = function(e) {
			if (req.readyState == 4 && req.status == 200) {
				if (req.status == 200) {
					// When ready, create a dictionary to return.
					var dict = {};
					var data = JSON.parse(req.responseText);
					// If there's no arrival data, then reply with -1.
					if (data.data[0] === undefined || data.data[0].arrivals[0] === undefined) {
						dict["CURRENT_VIEW_TIME"] = -1;
					} else {
						// Otherwise, calculate the minutes difference from now to the arrival estimate, and reply with that.
						var arrival = new Date(data.data[0].arrivals[0].arrival_at);
						var now = new Date();
						var minutes = Math.floor(((arrival - now) / 1000) / 60);
						dict["CURRENT_VIEW_TIME"] = minutes;
					}
					// Send the response.
					Pebble.sendAppMessage(dict);
				}
			}
		};
		req.send();
	}
	/*
	 * Listener and function called when the app first connects the phone.
	 */
Pebble.addEventListener("ready",
	function(e) {
		// App is connected to the phone.
		console.log("ready");
	});
/*
 * Listener and function are called when the phone receives a message from the app.
 * Messages are always a Tuple in the form of (route, stop).
 * All messages from the app to the phone are requesting time estimate, so parse the tuple,
 * and send it to getTimeEstimate().
 */
Pebble.addEventListener("appmessage",
	function(e) {
		var route = e.payload["PRESET_ROUTE_ID"];
		var stop = e.payload["PRESET_STOP_ID"];
		getTimeEstimate(route, stop);
	});

/*
 * Listener and function are called when the "Settings" button is selected in the Pebble app on the phone.
 * This opens an HTML page on my website which is designed for the URBuses config page.
 * If previous presets have been stored, the function sends these to the config page as well, so that
 * the user won't override the previous presets they've set.
 */
Pebble.addEventListener("showConfiguration",
	function(e) {
		var config_url = "http://tjstein.me/urbuses/urbuses_settings.html"; // Change this URL to the URL of your own configuration page.
		if (window.localStorage.getItem("presets") !== null) {
			var options = encodeURIComponent(window.localStorage.getItem("presets"));
			config_url = config_url + "?" + options;
		}
		Pebble.openURL(config_url);
	});

/*
 * Listener and function are called when the Config page is closed.
 * If the cancel button is selected, or they back out of the page without selecting anything,
 * the function simply returns and does nothing.
 * If they actually select presets, it is send along in JSON format.
 * This information is parse out into a dictionary of dictionaries, and sent to a function to send to the app.
 */
Pebble.addEventListener("webviewclosed",
	function(e) {
		// There is no response, so just end.
		if (decodeURIComponent(e.response) === "") {
			return;
		}
		// Otherwise, parse out the response from the app.
		var options = JSON.parse(decodeURIComponent(e.response));
		window.localStorage.setItem("presets", JSON.stringify(options));
		var dict = {};
		for (var i = 1; i <= 5; i++) {
			if (options[i + ""] !== undefined) {
				var dict2 = {};
				dict2["PRESET_NUMBER"] = i;
				if (options[i].route_id != 0) {
					dict2["PRESET_ROUTE_ID"] = parseInt(options[i].route_id);
					dict2["PRESET_ROUTE_NAME"] = options[i].route_name;
					dict2["PRESET_STOP_ID"] = parseInt(options[i].stop_id);
					dict2["PRESET_STOP_NAME"] = options[i].stop_name;
				} else {
					dict2["PRESET_ROUTE_ID"] = -1;
					dict2["PRESET_ROUTE_NAME"] = -1;
					dict2["PRESET_STOP_ID"] = -1;
					dict2["PRESET_STOP_NAME"] = -1;
				}
				dict[i] = dict2;
			}
		}
		sendStuff(dict, 1);
	});

var attempts = 0; // Variable to make sure we don't get stuck in a loop if we get nothing but Nacks back from Pebble.
/*
 * As the app can't handle more than one response at a time, this function uses the callback function of the
 * sendAppMessage function to recursively send each preset to the app.
 */
function sendStuff(dict, i) {
	var j = i;
	// Base case. Stop when this is reached.
	if (i > 5 || i < 0) {
		return;
	} else {
		if (dict[i] !== undefined) {
			Pebble.sendAppMessage(dict[i],
				function(e) {
					attempts = 0;
					j = j + 1;
					sendStuff(dict, j);
				},
				function(e) {
					console.log("ERROR: " + e.error.message);
					if (attempts > 5){
						console.log("Attempting to send again.");
						attempts++;
						sendStuff(dict, j);
					}
					else{
						console.log("Too many Nacks. Giving up.");
					}
				});
		}
		// Even if dict[i] doesn't exist, they may have set other presets.
		else {
			j = j + 1;
			sendStuff(dict, j);
		}
	}
}
<commit_msg>Added NACK handling to sending ETA<commit_after>/*
* NOTE: THE VARIABLE BELOW NEEDS A MASHAPE IDENTIFIER TO WORK.
* If you want to use this code in your own Transloc bus app, you need your own identifier
* To get a key, make an account at https://www.mashape.com/ and find your environment keys there.
*/
var identifier = "";
var agency = "283"; // This is the agency id for University of Rochester. If using another agency, change this variable to that id.
var attempts = 0; // Variable to make sure we don't get stuck in a loop if we get nothing but Nacks back from Pebble.
/*
 * Function to get the arrival time estimate for the given route stop combination.
 */
function getTimeEstimate(route, stop) {
		// If either the route or the stop are empty, then simply return -1 (no arrival estimate.)
		if (route == 0 || stop == 0) {
			Pebble.sendAppMessage({
				"CURRENT_VIEW_TIME": -1
			});
			return;
		}
		// Get in touch with the transloc service for the info.
		var req = new XMLHttpRequest();
		req.open('GET', 'https://transloc-api-1-2.p.mashape.com/arrival-estimates.json?agencies=' + agency + '&routes=' + route + '&stops=' + stop);
		req.setRequestHeader("X-Mashape-Authorization", identifier);
		req.onload = function(e) {
			if (req.readyState == 4 && req.status == 200) {
				if (req.status == 200) {
					// When ready, create a dictionary to return.
					var dict = {};
					var data = JSON.parse(req.responseText);
					// If there's no arrival data, then reply with -1.
					if (data.data[0] === undefined || data.data[0].arrivals[0] === undefined) {
						dict["CURRENT_VIEW_TIME"] = -1;
					} else {
						// Otherwise, calculate the minutes difference from now to the arrival estimate, and reply with that.
						var arrival = new Date(data.data[0].arrivals[0].arrival_at);
						var now = new Date();
						var minutes = Math.floor(((arrival - now) / 1000) / 60);
						dict["CURRENT_VIEW_TIME"] = minutes;
					}
					// Send the response.
					attempts = 0;
					sendETA(dict);
				}
			}
		};
		req.send();
	}

function sendETA(dict){
	Pebble.sendAppMessage(dict,
		function(e){
			console.log("Successfully sent ETA");
		},
		function(e){
			console.log("ERROR: " + e.error.message);
			if (attempts < 5){
				attempts++;
				console.log("Attempting to send again.");
				sendETA(dict);
			}
			else{
				console.log("Too many Nacks. Giving up.");
			}
		})
}

/*
 * Listener and function called when the app first connects the phone.
 */
Pebble.addEventListener("ready",
	function(e) {
		// App is connected to the phone.
		console.log("ready");
	});
/*
 * Listener and function are called when the phone receives a message from the app.
 * Messages are always a Tuple in the form of (route, stop).
 * All messages from the app to the phone are requesting time estimate, so parse the tuple,
 * and send it to getTimeEstimate().
 */
Pebble.addEventListener("appmessage",
	function(e) {
		var route = e.payload["PRESET_ROUTE_ID"];
		var stop = e.payload["PRESET_STOP_ID"];
		getTimeEstimate(route, stop);
	});

/*
 * Listener and function are called when the "Settings" button is selected in the Pebble app on the phone.
 * This opens an HTML page on my website which is designed for the URBuses config page.
 * If previous presets have been stored, the function sends these to the config page as well, so that
 * the user won't override the previous presets they've set.
 */
Pebble.addEventListener("showConfiguration",
	function(e) {
		var config_url = "http://tjstein.me/urbuses/urbuses_settings.html"; // Change this URL to the URL of your own configuration page.
		if (window.localStorage.getItem("presets") !== null) {
			var options = encodeURIComponent(window.localStorage.getItem("presets"));
			config_url = config_url + "?" + options;
		}
		Pebble.openURL(config_url);
	});

/*
 * Listener and function are called when the Config page is closed.
 * If the cancel button is selected, or they back out of the page without selecting anything,
 * the function simply returns and does nothing.
 * If they actually select presets, it is send along in JSON format.
 * This information is parse out into a dictionary of dictionaries, and sent to a function to send to the app.
 */
Pebble.addEventListener("webviewclosed",
	function(e) {
		// There is no response, so just end.
		if (decodeURIComponent(e.response) === "") {
			return;
		}
		// Otherwise, parse out the response from the app.
		var options = JSON.parse(decodeURIComponent(e.response));
		window.localStorage.setItem("presets", JSON.stringify(options));
		var dict = {};
		for (var i = 1; i <= 5; i++) {
			if (options[i + ""] !== undefined) {
				var dict2 = {};
				dict2["PRESET_NUMBER"] = i;
				if (options[i].route_id != 0) {
					dict2["PRESET_ROUTE_ID"] = parseInt(options[i].route_id);
					dict2["PRESET_ROUTE_NAME"] = options[i].route_name;
					dict2["PRESET_STOP_ID"] = parseInt(options[i].stop_id);
					dict2["PRESET_STOP_NAME"] = options[i].stop_name;
				} else {
					dict2["PRESET_ROUTE_ID"] = -1;
					dict2["PRESET_ROUTE_NAME"] = -1;
					dict2["PRESET_STOP_ID"] = -1;
					dict2["PRESET_STOP_NAME"] = -1;
				}
				dict[i] = dict2;
			}
		}
		attempts = 0;
		sendStuff(dict, 1);
	});

/*
 * As the app can't handle more than one response at a time, this function uses the callback function of the
 * sendAppMessage function to recursively send each preset to the app.
 */
function sendStuff(dict, i) {
	var j = i;
	// Base case. Stop when this is reached.
	if (i > 5 || i < 0) {
		return;
	} else {
		if (dict[i] !== undefined) {
			Pebble.sendAppMessage(dict[i],
				function(e) {
					attempts = 0;
					j = j + 1;
					sendStuff(dict, j);
				},
				function(e) {
					console.log("ERROR: " + e.error.message);
					if (attempts < 5){
						console.log("Attempting to send again.");
						attempts++;
						sendStuff(dict, j);
					}
					else{
						console.log("Too many Nacks. Giving up.");
					}
				});
		}
		// Even if dict[i] doesn't exist, they may have set other presets.
		else {
			j = j + 1;
			sendStuff(dict, j);
		}
	}
}
<|endoftext|>