<commit_before>/*
*
* Copyright (C) 2011, The Locker Project
* All rights reserved.
*
* Please see the LICENSE file for more information.
*
*/

/*
*
* Handles all sync logic of data from Twitter
*
*/

var request = require('request'),
    fs = require('fs'),
    locker = require('../../Common/node/locker.js'),
    lfs = require('../../Common/node/lfs.js'),
    EventEmitter = require('events').EventEmitter;
    dataStore = require('./dataStore');

var auth, userInfo, latests;
var twitterClient;
var allKnownIDs;
var requestCount = 0;

exports.eventEmitter = new EventEmitter();

// Initialize the state
exports.init = function(theAuth, callback) {
    auth = theAuth;
    try {
        latests = JSON.parse(fs.readFileSync('latests.json'));
    } catch (err) { latests = {}; }
    try {
        userInfo = JSON.parse(fs.readFileSync('userInfo.json'));
    } catch (err) { userInfo = {}; }
    try {
        allKnownIDs = JSON.parse(fs.readFileSync('allKnownIDs.json'));
    } catch (err) { allKnownIDs = {friends:{}, followers:{}}; }
    dataStore.init(function() {
        callback();
    });
}

// Pulls statuses from a given endpoint (home_timeline, mentions, etc via the /statuses twitter API endpoint)
exports.pullStatuses = function(endpoint, callback) {
    if(!getTwitterClient()) {
        sys.debug('could not get Twitter Client');
        callback('missing auth info :(');
        return;
    }
    if(!latests[endpoint])
        latests[endpoint] = {};
    var items = [];
    pullTimelinePage(endpoint, null, latests[endpoint].latest, null, items, function() {
        items.reverse();
        for(var i in items) {
            dataStore.addStatus(endpoint, items[i]);
            var eventObj = {source:endpoint, type:'new', status:items[i]};
            exports.eventEmitter.emit('status/twitter', eventObj);
        }
        callback(null, (endpoint === 'home_timeline' ? 60 : 120), "synced "+endpoint+" with "+items.length+" new entries");
    });
}

// Pulls one page of a statuses endpoint
function pullTimelinePage(endpoint, max_id, since_id, page, items, callback) {
    if(!page)
        page = 1;
    var params = {token: auth.token, count: 200, page: page, include_entities:true};
    if(max_id)
        params.max_id = max_id;
    if(since_id)
        params.since_id = since_id;
    requestCount++;
    getTwitterClient().apiCall('GET', '/statuses/' + endpoint + '.json', params, function(error, result) {
        if(error) {
            if(error.statusCode >= 500) { //failz-whalez, hang out for a bit
                setTimeout(function(){
                    pullTimelinePage(endpoint, max_id, since_id, page, items, callback);
                }, 10000);
            }
            require("sys").puts( error.stack )
            sys.debug('error from twitter:' + sys.inspect(error));
            return;
        }
        if(result.length > 0) {
            var id = result[0].id;
            if(!latests[endpoint].latest || id > latests[endpoint].latest)
                latests[endpoint].latest = id;
            for(var i = 0; i < result.length; i++)
                items.push(result[i]);

            if(!max_id)
                max_id = result[0].id;
            page++;
            if(requestCount > 300) {
                sys.debug('sleeping a bit...');
                setTimeout(function() {
                    pullTimelinePage(endpoint, max_id, since_id, page, items, callback);
                }, 30000);
            } else {
                pullTimelinePage(endpoint, max_id, since_id, page, items, callback);
            }
        } else if(callback) {
            lfs.writeObjectToFile('latests.json', latests);
            callback();
        }
    });
}

// Syncs info about friends of followers
exports.syncUsersInfo = function(friendsOrFollowers, callback) {
    if(!friendsOrFollowers || friendsOrFollowers.toLowerCase() != 'followers')
        friendsOrFollowers = 'friends';

    getUserInfo(function(err, newUserInfo) {
        userInfo = newUserInfo;
        lfs.writeObjectToFile('usersInfo.json', userInfo);
        getIDs(friendsOrFollowers, userInfo.screen_name, function(err, ids) {
            var newIDs = [];
            var knownIDs = allKnownIDs[friendsOrFollowers];
            var repeatedIDs = {};
            if(ids) {
                ids.forEach(function(id) {
                    if(!knownIDs[id])
                        newIDs.push(id);
                    else
                        repeatedIDs[id] = 1;
                });
            }
            var removedIDs = [];
            for(var knownID in knownIDs) {
                if(!repeatedIDs[knownID])
                    removedIDs.push(knownID);
            }
            if(newIDs.length < 1) {
                if(removedIDs.length > 0) {
                    logRemoved(friendsOrFollowers, removedIDs);
                    callback(null, 600, 'removed ' + removedIDs.length + friendsOrFolloers);
                }
                else {
                    callback(null, 600, 'synced 0 new ' + friendsOrFollowers);
                }
            } else {
                getUsersExtendedInfo(newIDs, function(usersInfo) {
                    addPeople(friendsOrFollowers, usersInfo, knownIDs);
                    if(removedIDs.length > 0)
                        logRemoved(friendsOrFollowers, removedIDs);
                    fs.writeFile('allKnownIDs.json', JSON.stringify(allKnownIDs));
                    callback(null, 600, 'synced ' + usersInfo.length + ' new ' + friendsOrFollowers);
                });
            }
        });
    });
}

exports.updateProfiles = function(type, callback) {
    if(!type || type.toLowerCase() != 'followers')
        type = 'friends';

    var ids = [];
    for(var i in allKnownIDs[type]) {
        ids.push(i);
    }
    getUsersExtendedInfo(ids, function(usersInfo) {
        updatePeople(type, usersInfo);
        callback();
    });
}

function updatePeople(type, people) {
    if(!people)
        return;
    people.forEach(function(profileFromTwitter) {
        dataStore.getPersonFromCurrent(type, profileFromTwitter.id, function(err, records) {
            if(err) {
                console.error('got error from dataStore.getPersonFromCurrent:', err);
            } else if(!records) {
                console.error('!records for type:', type, ' and id:', profileFromTwitter.id, '\nrecords:', records);
            } else if(records.length !== 1) {
                console.error('records.length !== 1 for type:', type, ' and id:', profileFromTwitter.id, '\nrecords:', records);
            } else {
                var profileFromSQL = JSON.parse(records[0].profile);
                var isDifferent = false;
                var keys = Object.keys(profileFromSQL);
                if(keys.length != Object.keys(profileFromTwitter).length) {
                    isDifferent = true;
                } else {
                    for(var key in profileFromTwitter) {
                        if(key === 'status') //don't check status
                            continue;
                        if(key !== 'status' && profileFromTwitter[key] !== profileFromSQL[key]) {
                            isDifferent = true;
                            break;
                        }
                    }
                }
                if(isDifferent) {
                    // console.error('found updated profile, orig:', profileFromSQL, '\nnew:', profileFromTwitter);
                    dataStore.logUpdatePerson(type, profileFromTwitter);
                    var eventObj = {source:type, type:'update', data:person};
                    exports.eventEmitter.emit('contact/twitter', eventObj);
                } else {
                    // console.error('no update, sql:', profileFromSQL.description, ', tw:', profileFromTwitter.description);
                }
            }
        })
    })
}

function addPeople(type, people, knownIDs) {
    for(var i in people) {
        var person = people[i];
        knownIDs[person.id_str] = 1;
        dataStore.addPerson(type, person);
        var eventObj = {source:type, type:'new', data:person};
        exports.eventEmitter.emit('contact/twitter', eventObj);
    }
}

function logRemoved(type, ids) {
    if(!ids)
        return;
    var knownIDs = allKnownIDs[type];
    ids.forEach(function(id) {
        dataStore.logRemovePerson(type, id);
        var eventObj = {source:type, type:'delete', data:{id:id, deleted:true}};
        exports.eventEmitter.emit('contact/twitter', eventObj);
        delete knownIDs[id];
    });
    fs.writeFile('allKnownIDs.json', JSON.stringify(allKnownIDs));
}

// Syncs the profile of the auth'd user
exports.syncProfile = function(callback) {
    getUserInfo(function(err, newUserInfo) {
        userInfo = newUserInfo;
        lfs.writeObjectToFile('userInfo.json', userInfo);
        callback(err, newUserInfo);
    });
}

// Gets the profile of the auth'd user
function getUserInfo(callback) {
    if(!getTwitterClient())
        return;
    getTwitterClient().apiCall('GET', '/account/verify_credentials.json',
                            {token:auth.token, include_entities:true}, callback);
}

// Gets the list of IDs of friends or followers of the auth'd user
function getIDs(friendsOrFolowers, screenName, callback) {
    if(!friendsOrFolowers || friendsOrFolowers.toLowerCase() != 'followers')
        friendsOrFolowers = 'friends';
    friendsOrFolowers = friendsOrFolowers.toLowerCase();
    getTwitterClient().apiCall('GET', '/' + friendsOrFolowers + '/ids.json',
                    {screen_name:screenName, cursor:-1, token: auth.token}, function(err, result) {
        if(err) {
            callback(err, result);
        } else {
            callback(null, result.ids);
        }
    });
}

// Get extended profile info about the users in userIDs
function getUsersExtendedInfo(userIDs, callback) {
    _getUsersExtendedInfo(userIDs, [], callback);
}

// Recursive function to handle the fact that twitter can only
// process 100 ID's at a time
// NOTE: there is a known bug in the Twitter API here!
// Returns some duplicates and misses others
function _getUsersExtendedInfo(userIDs, usersInfo, callback) {
    if(!usersInfo)
        usersInfo = [];
    var id_str = "";
    for(var i = 0; i < 100 && userIDs.length > 0; i++) {
        id_str += userIDs.pop();
        if(i < 99) id_str += ',';
    }
    getTwitterClient().apiCall('GET', '/users/lookup.json',
        {token: auth.token, user_id: id_str, include_entities: true},
        function(error, result) {
            if(error) {
                sys.debug('error! ' + JSON.stringify(error));
                return;
            }
            addAll(usersInfo, result.reverse());
            if(userIDs.length > 0)
                _getUsersExtendedInfo(userIDs, usersInfo, callback);
            else if(callback) {
                getPhotos(usersInfo);
                callback(usersInfo);
            }
        });
}

// Pulls profile images for a list of users
function getPhotos(users) {
    try {
        fs.mkdirSync('photos', 0755);
    } catch(err) {
    }
    var userz = [];
    for(var i in users)
        userz.push(users[i]);

    function _curlNext() {
        var user = userz.pop();
        if(!user)
            return;
        var photoExt = user.profile_image_url.substring(user.profile_image_url.lastIndexOf('/')+1);
        lfs.curlFile(user.profile_image_url, 'photos/' + user.id_str + photoExt, function(success) {
            _curlNext();
        });
    }
    _curlNext();
}

// Ensures that we are always working with the same, valid and auth'd twitter client object
function getTwitterClient() {
    if(!twitterClient && auth && auth.consumerKey && auth.consumerSecret)
        twitterClient = require('./twitter_client')(auth.consumerKey, auth.consumerSecret);
    return twitterClient;
}

/**
 *  returns object with:
 *  remaining_hits (api call remaining),
 *  hourly_limit (total allowed per hour),
 *  reset_time (time stamp),
 *  reset_time_in_seconds (unix time in secs)
 */
exports.getRateLimitStatus = function(callback) {
    request.get({uri:'http://api.twitter.com/1/account/rate_limit_status.json'}, function(err, resp, body) {
        var limits = JSON.parse(body);
        var remainingTime = limits.reset_time_in_seconds - (new Date().getTime() / 1000);
        if(limits.remaining_hits)
            limits.sec_between_calls = remainingTime / limits.remaining_hits;
        else
            limits.sec_between_calls = remainingTime / 1;
        callback(limits);
    });
}

// Concatenate arrays (is the some collection methods out there?)
function addAll(target, anotherArray) {
    if(!target)
        target = [];
    if(!anotherArray || !anotherArray.length)
        return;
    for(var i = 0; i < anotherArray.length; i++)
        target.push(anotherArray[i]);
}

// Nothing right now - will be part of calming routines
function clearCount() {
    requestCount = 0;
    setTimeout(clearCount, 3600000);
}
clearCount();<commit_msg>fixed typo in twitter updatePeople<commit_after>/*
*
* Copyright (C) 2011, The Locker Project
* All rights reserved.
*
* Please see the LICENSE file for more information.
*
*/

/*
*
* Handles all sync logic of data from Twitter
*
*/

var request = require('request'),
    fs = require('fs'),
    locker = require('../../Common/node/locker.js'),
    lfs = require('../../Common/node/lfs.js'),
    EventEmitter = require('events').EventEmitter;
    dataStore = require('./dataStore');

var auth, userInfo, latests;
var twitterClient;
var allKnownIDs;
var requestCount = 0;

exports.eventEmitter = new EventEmitter();

// Initialize the state
exports.init = function(theAuth, callback) {
    auth = theAuth;
    try {
        latests = JSON.parse(fs.readFileSync('latests.json'));
    } catch (err) { latests = {}; }
    try {
        userInfo = JSON.parse(fs.readFileSync('userInfo.json'));
    } catch (err) { userInfo = {}; }
    try {
        allKnownIDs = JSON.parse(fs.readFileSync('allKnownIDs.json'));
    } catch (err) { allKnownIDs = {friends:{}, followers:{}}; }
    dataStore.init(function() {
        callback();
    });
}

// Pulls statuses from a given endpoint (home_timeline, mentions, etc via the /statuses twitter API endpoint)
exports.pullStatuses = function(endpoint, callback) {
    if(!getTwitterClient()) {
        sys.debug('could not get Twitter Client');
        callback('missing auth info :(');
        return;
    }
    if(!latests[endpoint])
        latests[endpoint] = {};
    var items = [];
    pullTimelinePage(endpoint, null, latests[endpoint].latest, null, items, function() {
        items.reverse();
        for(var i in items) {
            dataStore.addStatus(endpoint, items[i]);
            var eventObj = {source:endpoint, type:'new', status:items[i]};
            exports.eventEmitter.emit('status/twitter', eventObj);
        }
        callback(null, (endpoint === 'home_timeline' ? 60 : 120), "synced "+endpoint+" with "+items.length+" new entries");
    });
}

// Pulls one page of a statuses endpoint
function pullTimelinePage(endpoint, max_id, since_id, page, items, callback) {
    if(!page)
        page = 1;
    var params = {token: auth.token, count: 200, page: page, include_entities:true};
    if(max_id)
        params.max_id = max_id;
    if(since_id)
        params.since_id = since_id;
    requestCount++;
    getTwitterClient().apiCall('GET', '/statuses/' + endpoint + '.json', params, function(error, result) {
        if(error) {
            if(error.statusCode >= 500) { //failz-whalez, hang out for a bit
                setTimeout(function(){
                    pullTimelinePage(endpoint, max_id, since_id, page, items, callback);
                }, 10000);
            }
            require("sys").puts( error.stack )
            sys.debug('error from twitter:' + sys.inspect(error));
            return;
        }
        if(result.length > 0) {
            var id = result[0].id;
            if(!latests[endpoint].latest || id > latests[endpoint].latest)
                latests[endpoint].latest = id;
            for(var i = 0; i < result.length; i++)
                items.push(result[i]);

            if(!max_id)
                max_id = result[0].id;
            page++;
            if(requestCount > 300) {
                sys.debug('sleeping a bit...');
                setTimeout(function() {
                    pullTimelinePage(endpoint, max_id, since_id, page, items, callback);
                }, 30000);
            } else {
                pullTimelinePage(endpoint, max_id, since_id, page, items, callback);
            }
        } else if(callback) {
            lfs.writeObjectToFile('latests.json', latests);
            callback();
        }
    });
}

// Syncs info about friends of followers
exports.syncUsersInfo = function(friendsOrFollowers, callback) {
    if(!friendsOrFollowers || friendsOrFollowers.toLowerCase() != 'followers')
        friendsOrFollowers = 'friends';

    getUserInfo(function(err, newUserInfo) {
        userInfo = newUserInfo;
        lfs.writeObjectToFile('usersInfo.json', userInfo);
        getIDs(friendsOrFollowers, userInfo.screen_name, function(err, ids) {
            var newIDs = [];
            var knownIDs = allKnownIDs[friendsOrFollowers];
            var repeatedIDs = {};
            if(ids) {
                ids.forEach(function(id) {
                    if(!knownIDs[id])
                        newIDs.push(id);
                    else
                        repeatedIDs[id] = 1;
                });
            }
            var removedIDs = [];
            for(var knownID in knownIDs) {
                if(!repeatedIDs[knownID])
                    removedIDs.push(knownID);
            }
            if(newIDs.length < 1) {
                if(removedIDs.length > 0) {
                    logRemoved(friendsOrFollowers, removedIDs);
                    callback(null, 600, 'removed ' + removedIDs.length + friendsOrFolloers);
                }
                else {
                    callback(null, 600, 'synced 0 new ' + friendsOrFollowers);
                }
            } else {
                getUsersExtendedInfo(newIDs, function(usersInfo) {
                    addPeople(friendsOrFollowers, usersInfo, knownIDs);
                    if(removedIDs.length > 0)
                        logRemoved(friendsOrFollowers, removedIDs);
                    fs.writeFile('allKnownIDs.json', JSON.stringify(allKnownIDs));
                    callback(null, 600, 'synced ' + usersInfo.length + ' new ' + friendsOrFollowers);
                });
            }
        });
    });
}

exports.updateProfiles = function(type, callback) {
    if(!type || type.toLowerCase() != 'followers')
        type = 'friends';

    var ids = [];
    for(var i in allKnownIDs[type]) {
        ids.push(i);
    }
    getUsersExtendedInfo(ids, function(usersInfo) {
        updatePeople(type, usersInfo);
        callback();
    });
}

function updatePeople(type, people) {
    if(!people)
        return;
    people.forEach(function(profileFromTwitter) {
        dataStore.getPersonFromCurrent(type, profileFromTwitter.id, function(err, records) {
            if(err) {
                console.error('got error from dataStore.getPersonFromCurrent:', err);
            } else if(!records) {
                console.error('!records for type:', type, ' and id:', profileFromTwitter.id, '\nrecords:', records);
            } else if(records.length !== 1) {
                console.error('records.length !== 1 for type:', type, ' and id:', profileFromTwitter.id, '\nrecords:', records);
            } else {
                var profileFromSQL = JSON.parse(records[0].profile);
                var isDifferent = false;
                var keys = Object.keys(profileFromSQL);
                if(keys.length != Object.keys(profileFromTwitter).length) {
                    isDifferent = true;
                } else {
                    for(var key in profileFromTwitter) {
                        if(key === 'status') //don't check status
                            continue;
                        if(key !== 'status' && profileFromTwitter[key] !== profileFromSQL[key]) {
                            isDifferent = true;
                            break;
                        }
                    }
                }
                if(isDifferent) {
                    // console.error('found updated profile, orig:', profileFromSQL, '\nnew:', profileFromTwitter);
                    dataStore.logUpdatePerson(type, profileFromTwitter);
                    var eventObj = {source:type, type:'update', data:profileFromTwitter};
                    exports.eventEmitter.emit('contact/twitter', eventObj);
                } else {
                    // console.error('no update, sql:', profileFromSQL.description, ', tw:', profileFromTwitter.description);
                }
            }
        })
    })
}

function addPeople(type, people, knownIDs) {
    for(var i in people) {
        var person = people[i];
        knownIDs[person.id_str] = 1;
        dataStore.addPerson(type, person);
        var eventObj = {source:type, type:'new', data:person};
        exports.eventEmitter.emit('contact/twitter', eventObj);
    }
}

function logRemoved(type, ids) {
    if(!ids)
        return;
    var knownIDs = allKnownIDs[type];
    ids.forEach(function(id) {
        dataStore.logRemovePerson(type, id);
        var eventObj = {source:type, type:'delete', data:{id:id, deleted:true}};
        exports.eventEmitter.emit('contact/twitter', eventObj);
        delete knownIDs[id];
    });
    fs.writeFile('allKnownIDs.json', JSON.stringify(allKnownIDs));
}

// Syncs the profile of the auth'd user
exports.syncProfile = function(callback) {
    getUserInfo(function(err, newUserInfo) {
        userInfo = newUserInfo;
        lfs.writeObjectToFile('userInfo.json', userInfo);
        callback(err, newUserInfo);
    });
}

// Gets the profile of the auth'd user
function getUserInfo(callback) {
    if(!getTwitterClient())
        return;
    getTwitterClient().apiCall('GET', '/account/verify_credentials.json',
                            {token:auth.token, include_entities:true}, callback);
}

// Gets the list of IDs of friends or followers of the auth'd user
function getIDs(friendsOrFolowers, screenName, callback) {
    if(!friendsOrFolowers || friendsOrFolowers.toLowerCase() != 'followers')
        friendsOrFolowers = 'friends';
    friendsOrFolowers = friendsOrFolowers.toLowerCase();
    getTwitterClient().apiCall('GET', '/' + friendsOrFolowers + '/ids.json',
                    {screen_name:screenName, cursor:-1, token: auth.token}, function(err, result) {
        if(err) {
            callback(err, result);
        } else {
            callback(null, result.ids);
        }
    });
}

// Get extended profile info about the users in userIDs
function getUsersExtendedInfo(userIDs, callback) {
    _getUsersExtendedInfo(userIDs, [], callback);
}

// Recursive function to handle the fact that twitter can only
// process 100 ID's at a time
// NOTE: there is a known bug in the Twitter API here!
// Returns some duplicates and misses others
function _getUsersExtendedInfo(userIDs, usersInfo, callback) {
    if(!usersInfo)
        usersInfo = [];
    var id_str = "";
    for(var i = 0; i < 100 && userIDs.length > 0; i++) {
        id_str += userIDs.pop();
        if(i < 99) id_str += ',';
    }
    getTwitterClient().apiCall('GET', '/users/lookup.json',
        {token: auth.token, user_id: id_str, include_entities: true},
        function(error, result) {
            if(error) {
                sys.debug('error! ' + JSON.stringify(error));
                return;
            }
            addAll(usersInfo, result.reverse());
            if(userIDs.length > 0)
                _getUsersExtendedInfo(userIDs, usersInfo, callback);
            else if(callback) {
                getPhotos(usersInfo);
                callback(usersInfo);
            }
        });
}

// Pulls profile images for a list of users
function getPhotos(users) {
    try {
        fs.mkdirSync('photos', 0755);
    } catch(err) {
    }
    var userz = [];
    for(var i in users)
        userz.push(users[i]);

    function _curlNext() {
        var user = userz.pop();
        if(!user)
            return;
        var photoExt = user.profile_image_url.substring(user.profile_image_url.lastIndexOf('/')+1);
        lfs.curlFile(user.profile_image_url, 'photos/' + user.id_str + photoExt, function(success) {
            _curlNext();
        });
    }
    _curlNext();
}

// Ensures that we are always working with the same, valid and auth'd twitter client object
function getTwitterClient() {
    if(!twitterClient && auth && auth.consumerKey && auth.consumerSecret)
        twitterClient = require('./twitter_client')(auth.consumerKey, auth.consumerSecret);
    return twitterClient;
}

/**
 *  returns object with:
 *  remaining_hits (api call remaining),
 *  hourly_limit (total allowed per hour),
 *  reset_time (time stamp),
 *  reset_time_in_seconds (unix time in secs)
 */
exports.getRateLimitStatus = function(callback) {
    request.get({uri:'http://api.twitter.com/1/account/rate_limit_status.json'}, function(err, resp, body) {
        var limits = JSON.parse(body);
        var remainingTime = limits.reset_time_in_seconds - (new Date().getTime() / 1000);
        if(limits.remaining_hits)
            limits.sec_between_calls = remainingTime / limits.remaining_hits;
        else
            limits.sec_between_calls = remainingTime / 1;
        callback(limits);
    });
}

// Concatenate arrays (is the some collection methods out there?)
function addAll(target, anotherArray) {
    if(!target)
        target = [];
    if(!anotherArray || !anotherArray.length)
        return;
    for(var i = 0; i < anotherArray.length; i++)
        target.push(anotherArray[i]);
}

// Nothing right now - will be part of calming routines
function clearCount() {
    requestCount = 0;
    setTimeout(clearCount, 3600000);
}
clearCount();<|endoftext|>


<commit_before>'use strict';

/**
 * @ngdoc service
 * @name polestar.Spec
 * @description
 * # Spec
 * Service in the polestar.
 */
angular.module('polestar')
  .service('Spec', function(_, vg, vl, cql, ZSchema, Alerts, Config, Dataset, Schema, Pills, Chart, consts, util, FilterManager, ANY, Logger) {

    var keys =  _.keys(Schema.schema.definitions.Encoding.properties);

    function instantiate() {
      return {
        data: Config.data,
        transform: {
          filterInvalid: undefined
        },
        mark: ANY,
        encoding: keys.reduce(function(e, c) {
          e[c] = {};
          return e;
        }, {}),
        config: Config.config
      };
    }

    var Spec = {
      /** @type {Object} verbose spec edited by the UI */
      spec: null,
      chart: Chart.getChart(null),
      isSpecific: true // Polestar always have specific spec (except for mark)
    };

    Spec._removeEmptyFieldDefs = function(spec) {
      spec.encoding = _.omit(spec.encoding, function(fieldDef, channel) {
        return !fieldDef || (fieldDef.field === undefined && fieldDef.value === undefined) ||
          (spec.mark && ! vl.channel.supportMark(channel, spec.mark));
      });
    };

    function deleteNulls(obj) {
      for (var prop in obj) {
        if (_.isObject(obj[prop])) {
          deleteNulls(obj[prop]);
        }
        // This is why I hate js
        if (obj[prop] === null ||
          obj[prop] === undefined ||
          (
            // In general, {} should be removed from spec. bin:{} is an exception.
            _.isObject(obj[prop]) &&
            vg.util.keys(obj[prop]).length === 0 &&
            prop !== 'bin'
          ) ||
          obj[prop] === []) {
          delete obj[prop];
        }
      }
    }

    function parse(spec) {
      var oldSpec = util.duplicate(spec);
      var oldFilter = null;

      if (oldSpec) {
        // Store oldFilter, copy oldSpec that exclude transform.filter
        oldFilter = (oldSpec.transform || {}).filter;
        var transform = _.omit(oldSpec.transform || {}, 'filter');
        oldSpec = _.omit(oldSpec, 'transform');
        if (transform) {
          oldSpec.transform = transform;
        }
      }

      var newSpec = vl.util.mergeDeep(instantiate(), oldSpec);

      // This is not Vega-Lite filter object, but rather our FilterModel
      newSpec.transform.filter = FilterManager.reset(oldFilter);

      return newSpec;
    }

    // takes a partial spec
    Spec.parseSpec = function(newSpec) {
      // TODO: revise this
      Spec.spec = parse(newSpec);
    };

    Spec.reset = function(hard) {
      var spec = instantiate();
      spec.transform.filter = FilterManager.reset(hard);
      Spec.spec = spec;
    };

    /**
     * Takes a full spec, validates it and then rebuilds all members of the chart object.
     */
    Spec.update = function(spec) {
      spec = _.cloneDeep(spec || Spec.spec);

      Spec._removeEmptyFieldDefs(spec);
      deleteNulls(spec);

      if (spec.transform && spec.transform.filter) {
        delete spec.transform.filter;
      }

      spec.transform = spec.transform || {};
      var filter = FilterManager.getVlFilter();
      if (filter || spec.transform.filter) {
        spec.transform.filter = filter;
      }

      // we may have removed encoding
      if (!('encoding' in spec)) {
        spec.encoding = {};
      }
      if (!('config' in spec)) {
        spec.config = {};
      }
      // var validator = new ZSchema();

      // validator.setRemoteReference('http://json-schema.org/draft-04/schema', {});

      // var schema = Schema.schema;

      // ZSchema.registerFormat('color', function (str) {
      //   // valid colors are in list or hex color
      //   return /^#([0-9a-f]{3}){1,2}$/i.test(str);
      //   // TODO: support color name
      // });
      // ZSchema.registerFormat('font', function () {
      //   // right now no fonts are valid
      //   return false;
      // });

      // // now validate the spec
      // var valid = validator.validate(spec, schema);

      // if (!valid) {
      //   //FIXME: move this dependency to directive/controller layer
      //   Alerts.add({
      //     msg: validator.getLastErrors()
      //   });
      // } else {
        vg.util.extend(spec.config, Config.small());

        if (!Dataset.schema) { return Spec; }

        var query = Spec.cleanQuery = getQuery(spec);
        if (_.isEqual(query, Spec.oldCleanQuery)) {
          return Spec; // no need to update charts
        }
        Spec.oldCleanQuery = _.cloneDeep(query);
        var output = cql.query(query, Dataset.schema);
        Spec.query = output.query;
        var topItem = output.result.getTopSpecQueryModel();
        Spec.chart = Chart.getChart(topItem);
    // }
      return Spec;
    };

    function getSpecQuery(spec, convertFilter /*HACK*/) {
      if (convertFilter) {
        spec = util.duplicate(spec);

        // HACK convert filter manager to proper filter spec
        if (spec.transform && spec.transform.filter) {
          delete spec.transform.filter;
        }

        var filter = FilterManager.getVlFilter();
        if (filter) {
          spec.transform = spec.transform || {};
          spec.transform.filter = filter;
        }
      }

      return {
        data: Config.data,
        mark: spec.mark === ANY ? '?' : spec.mark,

        // TODO: support transform enumeration
        transform: spec.transform,
        encodings: vg.util.keys(spec.encoding).reduce(function(encodings, channelId) {
          var encQ = vg.util.extend(
            // Add channel
            { channel: Pills.isAnyChannel(channelId) ? '?' : channelId },
            // Field Def
            spec.encoding[channelId],
            // Remove Title
            {title: undefined}
          );

          if (cql.enumSpec.isEnumSpec(encQ.field)) {
            // replace the name so we should it's the field from this channelId
            encQ.field = {
              name: 'f' + channelId,
              enum: encQ.field.enum
            };
          }

          encodings.push(encQ);
          return encodings;
        }, []),
        config: spec.config
      };
    }

    function getQuery(spec, convertFilter /*HACK */) {
      var specQuery = getSpecQuery(spec, convertFilter);

      var hasAnyField = _.some(specQuery.encodings, function(encQ) {
        return cql.enumSpec.isEnumSpec(encQ.field);
      });

      var groupBy = hasAnyField ?
        ['field', 'aggregate', 'bin', 'timeUnit', 'stack'] :
        ['field', 'aggregate', 'bin', 'timeUnit', 'stack', 'channel']; // do not group by mark

      return {
        spec: specQuery,
        groupBy: groupBy,
        orderBy: ['aggregationQuality', 'effectiveness'], // FIXME add field order
        chooseBy: 'effectiveness',
        config: {
          omitTableWithOcclusion: false
        }
      };
    }

    function instantiatePill(channel) { // jshint ignore:line
      return {};
    }

    /** copy value from the pill to the fieldDef */
    function updateChannelDef(encoding, pill, channel){
      var type = pill.type,
        supportedRole = vl.channel.getSupportedRole(channel),
        dimensionOnly = supportedRole.dimension && !supportedRole.measure;

      // auto cast binning / time binning for dimension only encoding type.
      if (pill.field && dimensionOnly) {
        if (pill.aggregate==='count') {
          pill = {};
        } else if (type === vl.type.QUANTITATIVE && !pill.bin) {
          pill.aggregate = undefined;
          pill.bin = {maxbins: vl.bin.MAXBINS_DEFAULT};
        } else if(type === vl.type.TEMPORAL && !pill.timeUnit) {
          pill.timeUnit = consts.defaultTimeFn;
        }
      } else if (!pill.field) {
        // no field, it's actually the empty shelf that
        // got processed in the opposite direction
        pill = {};
      }

      // filter unsupported properties
      var fieldDef = instantiatePill(channel),
        shelfProps = Schema.getChannelSchema(channel).properties;

      for (var prop in shelfProps) {
        if (pill[prop]) {
          if (prop==='value' && pill.field) {
            // only copy value if field is not defined
            // (which should never be the case)
            delete fieldDef[prop];
          } else {
            //FXIME In some case this should be merge / recursive merge instead ?
            fieldDef[prop] = pill[prop];
          }
        }
      }
      encoding[channel] = fieldDef;
    }

    Pills.listener = {
      set: function(channelId, pill) {
        updateChannelDef(Spec.spec.encoding, pill, channelId);
      },
      remove: function(channelId) {
        updateChannelDef(Spec.spec.encoding, {}, channelId); // remove all pill detail from the fieldDef
      },
      add: function(fieldDef) {
        var oldMarkIsEnumSpec = cql.enumSpec.isEnumSpec(Spec.cleanQuery.spec.mark);

        Logger.logInteraction(Logger.actions.ADD_FIELD, fieldDef, {
          from: Spec.chart.shorthand
        });

        if (Spec.isSpecific && !cql.enumSpec.isEnumSpec(fieldDef.field)) {
          // Call CompassQL to run query and load the top-ranked result
          var specQuery = Spec.cleanQuery.spec;
          var encQ = _.extend(
            {},
            fieldDef,
            {
              channel: cql.enumSpec.SHORT_ENUM_SPEC
            },
            fieldDef.aggregate === 'count' ? {} : {
              aggregate: cql.enumSpec.SHORT_ENUM_SPEC,
              bin: cql.enumSpec.SHORT_ENUM_SPEC,
              timeUnit: cql.enumSpec.SHORT_ENUM_SPEC
            }
          );
          specQuery.encodings.push(encQ);

          var query = {
            spec: specQuery,
            groupBy: ['field', 'aggregate', 'bin', 'timeUnit', 'stack'],
            orderBy: 'aggregationQuality',
            chooseBy: 'effectiveness',
            config: {omitTableWithOcclusion: false}
          };

          var output = cql.query(query, Dataset.schema);
          var result = output.result;

          var topItem = result.getTopSpecQueryModel();

          if (!topItem) {
            // No Top Item
            Alerts.add('Cannot automatically adding this field anymore');
            return;
          }

          // The top spec will always have specific mark.
          // We need to restore the mark to ANY if applicable.
          var topSpec = topItem.toSpec();
          if (oldMarkIsEnumSpec) {
            topSpec.mark = ANY;
          }
          Spec.parseSpec(topSpec);
        } else {
          var encoding = _.clone(Spec.spec.encoding);
          // Just add to any channel because CompassQL do not support partial filling yet.
          var emptyAnyChannel = Pills.getEmptyAnyChannelId();
          updateChannelDef(encoding, _.clone(fieldDef), emptyAnyChannel);

          // Add new any as a placeholder
          var newAnyChannel = Pills.getNextAnyChannelId();
          if (newAnyChannel !== null) {
            updateChannelDef(encoding, {}, newAnyChannel);
          }

          Spec.spec.encoding = encoding;
        }

      },
      select: function(spec) {
        var specQuery = getSpecQuery(spec);
        specQuery.mark = '?';

        var query = {
          spec: specQuery,
          chooseBy: 'effectiveness'
        };
        var output = cql.query(query, Dataset.schema);
        var result = output.result;

        if (result.getTopSpecQueryModel().getMark() === spec.mark) {
          // make a copy and replace mark with '?'
          spec = util.duplicate(spec);
          spec.mark = ANY;
        }
        Spec.parseSpec(spec);
      },
      parse: function(spec) {
        Spec.parseSpec(spec);
      },
      update: function(spec) {
        Spec.update(spec);
      },
      reset: function() {
        Spec.reset();
      },
      dragDrop: function(cidDragTo, cidDragFrom) {
        // Make a copy and update the clone of the encoding to prevent glitches
        var encoding = _.clone(Spec.spec.encoding);
        // console.log('dragDrop', encoding, Pills, 'from:', cidDragFrom, Pills.get(cidDragFrom));

        // If pill is dragged from another shelf, not the schemalist
        if (cidDragFrom) {
          // console.log('pillDragFrom', Pills.get(cidDragFrom));
          updateChannelDef(encoding, Pills.get(cidDragFrom) || {}, cidDragFrom);
        }
        updateChannelDef(encoding, Pills.get(cidDragTo) || {}, cidDragTo);

        // console.log('Pills.dragDrop',
        //   'from:', cidDragFrom, Pills.get(cidDragFrom), encoding[cidDragFrom],
        //   'to:', cidDragTo, Pills.get(cidDragTo), encoding[cidDragTo]);

        // Finally, update the encoding only once to prevent glitches
        Spec.spec.encoding = encoding;
      },
      rescale: function (channelId, scaleType) {
        var fieldDef = Spec.spec.encoding[channelId];
        if (fieldDef.scale) {
          fieldDef.scale.type = scaleType;
        } else {
          fieldDef.scale = {type: scaleType};
        }
      },
      sort: function(channelId, sort) {
        Spec.spec.encoding[channelId].sort = sort;
      },
      transpose: function() {
        Chart.transpose(Spec.spec);
      },
      toggleFilterInvalid: function () {
        Spec.spec.transform.filterInvalid = Spec.spec.transform.filterInvalid ? undefined : true;
      }
    };

    Spec.reset();
    Dataset.onUpdate.push(function() {
      Spec.reset(true);
    });

    return Spec;
  });
<commit_msg>[Hard reset filter when we change dataset] -- followup<commit_after>'use strict';

/**
 * @ngdoc service
 * @name polestar.Spec
 * @description
 * # Spec
 * Service in the polestar.
 */
angular.module('polestar')
  .service('Spec', function(_, vg, vl, cql, ZSchema, Alerts, Config, Dataset, Schema, Pills, Chart, consts, util, FilterManager, ANY, Logger) {

    var keys =  _.keys(Schema.schema.definitions.Encoding.properties);

    function instantiate() {
      return {
        data: Config.data,
        transform: {
          filterInvalid: undefined
        },
        mark: ANY,
        encoding: keys.reduce(function(e, c) {
          e[c] = {};
          return e;
        }, {}),
        config: Config.config
      };
    }

    var Spec = {
      /** @type {Object} verbose spec edited by the UI */
      spec: null,
      chart: Chart.getChart(null),
      isSpecific: true // Polestar always have specific spec (except for mark)
    };

    Spec._removeEmptyFieldDefs = function(spec) {
      spec.encoding = _.omit(spec.encoding, function(fieldDef, channel) {
        return !fieldDef || (fieldDef.field === undefined && fieldDef.value === undefined) ||
          (spec.mark && ! vl.channel.supportMark(channel, spec.mark));
      });
    };

    function deleteNulls(obj) {
      for (var prop in obj) {
        if (_.isObject(obj[prop])) {
          deleteNulls(obj[prop]);
        }
        // This is why I hate js
        if (obj[prop] === null ||
          obj[prop] === undefined ||
          (
            // In general, {} should be removed from spec. bin:{} is an exception.
            _.isObject(obj[prop]) &&
            vg.util.keys(obj[prop]).length === 0 &&
            prop !== 'bin'
          ) ||
          obj[prop] === []) {
          delete obj[prop];
        }
      }
    }

    function parse(spec) {
      var oldSpec = util.duplicate(spec);
      var oldFilter = null;

      if (oldSpec) {
        // Store oldFilter, copy oldSpec that exclude transform.filter
        oldFilter = (oldSpec.transform || {}).filter;
        var transform = _.omit(oldSpec.transform || {}, 'filter');
        oldSpec = _.omit(oldSpec, 'transform');
        if (transform) {
          oldSpec.transform = transform;
        }
      }

      var newSpec = vl.util.mergeDeep(instantiate(), oldSpec);

      // This is not Vega-Lite filter object, but rather our FilterModel
      newSpec.transform.filter = FilterManager.reset(oldFilter);

      return newSpec;
    }

    // takes a partial spec
    Spec.parseSpec = function(newSpec) {
      // TODO: revise this
      Spec.spec = parse(newSpec);
    };

    Spec.reset = function(hard) {
      var spec = instantiate();
      spec.transform.filter = FilterManager.reset(null, hard);
      Spec.spec = spec;
    };

    /**
     * Takes a full spec, validates it and then rebuilds all members of the chart object.
     */
    Spec.update = function(spec) {
      spec = _.cloneDeep(spec || Spec.spec);

      Spec._removeEmptyFieldDefs(spec);
      deleteNulls(spec);

      if (spec.transform && spec.transform.filter) {
        delete spec.transform.filter;
      }

      spec.transform = spec.transform || {};
      var filter = FilterManager.getVlFilter();
      if (filter || spec.transform.filter) {
        spec.transform.filter = filter;
      }

      // we may have removed encoding
      if (!('encoding' in spec)) {
        spec.encoding = {};
      }
      if (!('config' in spec)) {
        spec.config = {};
      }
      // var validator = new ZSchema();

      // validator.setRemoteReference('http://json-schema.org/draft-04/schema', {});

      // var schema = Schema.schema;

      // ZSchema.registerFormat('color', function (str) {
      //   // valid colors are in list or hex color
      //   return /^#([0-9a-f]{3}){1,2}$/i.test(str);
      //   // TODO: support color name
      // });
      // ZSchema.registerFormat('font', function () {
      //   // right now no fonts are valid
      //   return false;
      // });

      // // now validate the spec
      // var valid = validator.validate(spec, schema);

      // if (!valid) {
      //   //FIXME: move this dependency to directive/controller layer
      //   Alerts.add({
      //     msg: validator.getLastErrors()
      //   });
      // } else {
        vg.util.extend(spec.config, Config.small());

        if (!Dataset.schema) { return Spec; }

        var query = Spec.cleanQuery = getQuery(spec);
        if (_.isEqual(query, Spec.oldCleanQuery)) {
          return Spec; // no need to update charts
        }
        Spec.oldCleanQuery = _.cloneDeep(query);
        var output = cql.query(query, Dataset.schema);
        Spec.query = output.query;
        var topItem = output.result.getTopSpecQueryModel();
        Spec.chart = Chart.getChart(topItem);
    // }
      return Spec;
    };

    function getSpecQuery(spec, convertFilter /*HACK*/) {
      if (convertFilter) {
        spec = util.duplicate(spec);

        // HACK convert filter manager to proper filter spec
        if (spec.transform && spec.transform.filter) {
          delete spec.transform.filter;
        }

        var filter = FilterManager.getVlFilter();
        if (filter) {
          spec.transform = spec.transform || {};
          spec.transform.filter = filter;
        }
      }

      return {
        data: Config.data,
        mark: spec.mark === ANY ? '?' : spec.mark,

        // TODO: support transform enumeration
        transform: spec.transform,
        encodings: vg.util.keys(spec.encoding).reduce(function(encodings, channelId) {
          var encQ = vg.util.extend(
            // Add channel
            { channel: Pills.isAnyChannel(channelId) ? '?' : channelId },
            // Field Def
            spec.encoding[channelId],
            // Remove Title
            {title: undefined}
          );

          if (cql.enumSpec.isEnumSpec(encQ.field)) {
            // replace the name so we should it's the field from this channelId
            encQ.field = {
              name: 'f' + channelId,
              enum: encQ.field.enum
            };
          }

          encodings.push(encQ);
          return encodings;
        }, []),
        config: spec.config
      };
    }

    function getQuery(spec, convertFilter /*HACK */) {
      var specQuery = getSpecQuery(spec, convertFilter);

      var hasAnyField = _.some(specQuery.encodings, function(encQ) {
        return cql.enumSpec.isEnumSpec(encQ.field);
      });

      var groupBy = hasAnyField ?
        ['field', 'aggregate', 'bin', 'timeUnit', 'stack'] :
        ['field', 'aggregate', 'bin', 'timeUnit', 'stack', 'channel']; // do not group by mark

      return {
        spec: specQuery,
        groupBy: groupBy,
        orderBy: ['aggregationQuality', 'effectiveness'], // FIXME add field order
        chooseBy: 'effectiveness',
        config: {
          omitTableWithOcclusion: false
        }
      };
    }

    function instantiatePill(channel) { // jshint ignore:line
      return {};
    }

    /** copy value from the pill to the fieldDef */
    function updateChannelDef(encoding, pill, channel){
      var type = pill.type,
        supportedRole = vl.channel.getSupportedRole(channel),
        dimensionOnly = supportedRole.dimension && !supportedRole.measure;

      // auto cast binning / time binning for dimension only encoding type.
      if (pill.field && dimensionOnly) {
        if (pill.aggregate==='count') {
          pill = {};
        } else if (type === vl.type.QUANTITATIVE && !pill.bin) {
          pill.aggregate = undefined;
          pill.bin = {maxbins: vl.bin.MAXBINS_DEFAULT};
        } else if(type === vl.type.TEMPORAL && !pill.timeUnit) {
          pill.timeUnit = consts.defaultTimeFn;
        }
      } else if (!pill.field) {
        // no field, it's actually the empty shelf that
        // got processed in the opposite direction
        pill = {};
      }

      // filter unsupported properties
      var fieldDef = instantiatePill(channel),
        shelfProps = Schema.getChannelSchema(channel).properties;

      for (var prop in shelfProps) {
        if (pill[prop]) {
          if (prop==='value' && pill.field) {
            // only copy value if field is not defined
            // (which should never be the case)
            delete fieldDef[prop];
          } else {
            //FXIME In some case this should be merge / recursive merge instead ?
            fieldDef[prop] = pill[prop];
          }
        }
      }
      encoding[channel] = fieldDef;
    }

    Pills.listener = {
      set: function(channelId, pill) {
        updateChannelDef(Spec.spec.encoding, pill, channelId);
      },
      remove: function(channelId) {
        updateChannelDef(Spec.spec.encoding, {}, channelId); // remove all pill detail from the fieldDef
      },
      add: function(fieldDef) {
        var oldMarkIsEnumSpec = cql.enumSpec.isEnumSpec(Spec.cleanQuery.spec.mark);

        Logger.logInteraction(Logger.actions.ADD_FIELD, fieldDef, {
          from: Spec.chart.shorthand
        });

        if (Spec.isSpecific && !cql.enumSpec.isEnumSpec(fieldDef.field)) {
          // Call CompassQL to run query and load the top-ranked result
          var specQuery = Spec.cleanQuery.spec;
          var encQ = _.extend(
            {},
            fieldDef,
            {
              channel: cql.enumSpec.SHORT_ENUM_SPEC
            },
            fieldDef.aggregate === 'count' ? {} : {
              aggregate: cql.enumSpec.SHORT_ENUM_SPEC,
              bin: cql.enumSpec.SHORT_ENUM_SPEC,
              timeUnit: cql.enumSpec.SHORT_ENUM_SPEC
            }
          );
          specQuery.encodings.push(encQ);

          var query = {
            spec: specQuery,
            groupBy: ['field', 'aggregate', 'bin', 'timeUnit', 'stack'],
            orderBy: 'aggregationQuality',
            chooseBy: 'effectiveness',
            config: {omitTableWithOcclusion: false}
          };

          var output = cql.query(query, Dataset.schema);
          var result = output.result;

          var topItem = result.getTopSpecQueryModel();

          if (!topItem) {
            // No Top Item
            Alerts.add('Cannot automatically adding this field anymore');
            return;
          }

          // The top spec will always have specific mark.
          // We need to restore the mark to ANY if applicable.
          var topSpec = topItem.toSpec();
          if (oldMarkIsEnumSpec) {
            topSpec.mark = ANY;
          }
          Spec.parseSpec(topSpec);
        } else {
          var encoding = _.clone(Spec.spec.encoding);
          // Just add to any channel because CompassQL do not support partial filling yet.
          var emptyAnyChannel = Pills.getEmptyAnyChannelId();
          updateChannelDef(encoding, _.clone(fieldDef), emptyAnyChannel);

          // Add new any as a placeholder
          var newAnyChannel = Pills.getNextAnyChannelId();
          if (newAnyChannel !== null) {
            updateChannelDef(encoding, {}, newAnyChannel);
          }

          Spec.spec.encoding = encoding;
        }

      },
      select: function(spec) {
        var specQuery = getSpecQuery(spec);
        specQuery.mark = '?';

        var query = {
          spec: specQuery,
          chooseBy: 'effectiveness'
        };
        var output = cql.query(query, Dataset.schema);
        var result = output.result;

        if (result.getTopSpecQueryModel().getMark() === spec.mark) {
          // make a copy and replace mark with '?'
          spec = util.duplicate(spec);
          spec.mark = ANY;
        }
        Spec.parseSpec(spec);
      },
      parse: function(spec) {
        Spec.parseSpec(spec);
      },
      update: function(spec) {
        Spec.update(spec);
      },
      reset: function() {
        Spec.reset();
      },
      dragDrop: function(cidDragTo, cidDragFrom) {
        // Make a copy and update the clone of the encoding to prevent glitches
        var encoding = _.clone(Spec.spec.encoding);
        // console.log('dragDrop', encoding, Pills, 'from:', cidDragFrom, Pills.get(cidDragFrom));

        // If pill is dragged from another shelf, not the schemalist
        if (cidDragFrom) {
          // console.log('pillDragFrom', Pills.get(cidDragFrom));
          updateChannelDef(encoding, Pills.get(cidDragFrom) || {}, cidDragFrom);
        }
        updateChannelDef(encoding, Pills.get(cidDragTo) || {}, cidDragTo);

        // console.log('Pills.dragDrop',
        //   'from:', cidDragFrom, Pills.get(cidDragFrom), encoding[cidDragFrom],
        //   'to:', cidDragTo, Pills.get(cidDragTo), encoding[cidDragTo]);

        // Finally, update the encoding only once to prevent glitches
        Spec.spec.encoding = encoding;
      },
      rescale: function (channelId, scaleType) {
        var fieldDef = Spec.spec.encoding[channelId];
        if (fieldDef.scale) {
          fieldDef.scale.type = scaleType;
        } else {
          fieldDef.scale = {type: scaleType};
        }
      },
      sort: function(channelId, sort) {
        Spec.spec.encoding[channelId].sort = sort;
      },
      transpose: function() {
        Chart.transpose(Spec.spec);
      },
      toggleFilterInvalid: function () {
        Spec.spec.transform.filterInvalid = Spec.spec.transform.filterInvalid ? undefined : true;
      }
    };

    Spec.reset();
    Dataset.onUpdate.push(function() {
      Spec.reset(true);
    });

    return Spec;
  });
<|endoftext|>


<commit_before>import { Buffer } from 'node:buffer'
import InvocationsController from './InvocationsController.js'

const { parse } = JSON

// https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html
export default function invocationsRoute(lambda, options) {
  const invocationsController = new InvocationsController(lambda)

  return {
    async handler(request, h) {
      const {
        headers,
        params: { functionName },
        payload,
      } = request

      const _headers = new Headers(headers)

      const clientContextHeader = _headers.get('x-amz-client-context')
      const invocationType = _headers.get('x-amz-invocation-type')

      // default is undefined
      let clientContext

      // check client context header was set
      if (clientContextHeader) {
        const clientContextBuffer = Buffer.from(clientContextHeader, 'base64')
        clientContext = parse(clientContextBuffer.toString('utf-8'))
      }

      // check if payload was set, if not, default event is an empty object
      const event = payload.length > 0 ? parse(payload.toString('utf-8')) : {}

      const invokeResults = await invocationsController.invoke(
        functionName,
        invocationType,
        event,
        clientContext,
      )

      // Return with correct status codes
      let resultPayload = ''
      let statusCode = 200
      let functionError = null
      if (invokeResults) {
        const isPayloadDefined = typeof invokeResults.Payload !== 'undefined'
        resultPayload = isPayloadDefined ? invokeResults.Payload : ''
        statusCode = invokeResults.StatusCode || 200
        functionError = invokeResults.FunctionError || null
      }
      const response = h.response(resultPayload).code(statusCode)
      if (functionError) {
        // AWS Invoke documentation is wrong. The header for error type is
        // 'x-amzn-ErrorType' in production, not 'X-Amz-Function-Error'
        response.header('x-amzn-ErrorType', functionError)
      }
      if (invokeResults && invokeResults.UnhandledError) {
        response.header('X-Amz-Function-Error', 'Unhandled')
      }
      return response
    },
    method: 'POST',
    options: {
      cors: options.corsConfig,
      payload: {
        // allow: ['binary/octet-stream'],
        defaultContentType: 'binary/octet-stream',
        // request.payload will be a raw buffer
        parse: false,
      },
      tags: ['api'],
    },
    path: '/2015-03-31/functions/{functionName}/invocations',
  }
}
<commit_msg>fix: Increase invocation payload limit to 6 mb<commit_after>import { Buffer } from 'node:buffer'
import InvocationsController from './InvocationsController.js'

const { parse } = JSON

// https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html
export default function invocationsRoute(lambda, options) {
  const invocationsController = new InvocationsController(lambda)

  return {
    async handler(request, h) {
      const {
        headers,
        params: { functionName },
        payload,
      } = request

      const _headers = new Headers(headers)

      const clientContextHeader = _headers.get('x-amz-client-context')
      const invocationType = _headers.get('x-amz-invocation-type')

      // default is undefined
      let clientContext

      // check client context header was set
      if (clientContextHeader) {
        const clientContextBuffer = Buffer.from(clientContextHeader, 'base64')
        clientContext = parse(clientContextBuffer.toString('utf-8'))
      }

      // check if payload was set, if not, default event is an empty object
      const event = payload.length > 0 ? parse(payload.toString('utf-8')) : {}

      const invokeResults = await invocationsController.invoke(
        functionName,
        invocationType,
        event,
        clientContext,
      )

      // Return with correct status codes
      let resultPayload = ''
      let statusCode = 200
      let functionError = null
      if (invokeResults) {
        const isPayloadDefined = typeof invokeResults.Payload !== 'undefined'
        resultPayload = isPayloadDefined ? invokeResults.Payload : ''
        statusCode = invokeResults.StatusCode || 200
        functionError = invokeResults.FunctionError || null
      }
      const response = h.response(resultPayload).code(statusCode)
      if (functionError) {
        // AWS Invoke documentation is wrong. The header for error type is
        // 'x-amzn-ErrorType' in production, not 'X-Amz-Function-Error'
        response.header('x-amzn-ErrorType', functionError)
      }
      if (invokeResults && invokeResults.UnhandledError) {
        response.header('X-Amz-Function-Error', 'Unhandled')
      }
      return response
    },
    method: 'POST',
    options: {
      cors: options.corsConfig,
      payload: {
        // allow: ['binary/octet-stream'],
        defaultContentType: 'binary/octet-stream',
        // Set maximum size to 6 MB to match maximum invocation payload size in synchronous responses
        maxBytes: 1024 * 1024 * 6,
        // request.payload will be a raw buffer
        parse: false,
      },
      tags: ['api'],
    },
    path: '/2015-03-31/functions/{functionName}/invocations',
  }
}
<|endoftext|>


<commit_before>(function() {
    var Ext = window.Ext4 || window.Ext;

    Ext.define('Rally.apps.kanban.KanbanApp', {
        extend: 'Rally.app.App',
        requires: [
            'Rally.apps.kanban.Settings',
            'Rally.apps.kanban.Column',
            'Rally.ui.gridboard.GridBoard',
            'Rally.ui.gridboard.plugin.GridBoardAddNew',

            'Rally.ui.gridboard.plugin.BoardPolicyDisplayable',
            'Rally.ui.cardboard.plugin.ColumnPolicy',
            'Rally.ui.cardboard.PolicyContainer',
            'Rally.ui.cardboard.CardBoard',
            'Rally.ui.cardboard.plugin.Scrollable',
            'Rally.ui.report.StandardReport',
            'Rally.clientmetrics.ClientMetricsRecordable',
            'Rally.ui.gridboard.plugin.GridBoardCustomFilterControl',
            'Rally.ui.gridboard.plugin.GridBoardFieldPicker',
            'Rally.ui.cardboard.plugin.FixedHeader'
        ],
        mixins: [
            'Rally.clientmetrics.ClientMetricsRecordable'
        ],
        cls: 'kanban',
        alias: 'widget.kanbanapp',
        appName: 'Kanban',

        settingsScope: 'project',

        items: [
			{
				xtype: 'container',
				itemId: 'slaContainer'
			},
            {
                xtype: 'container',
                itemId: 'bodyContainer'
            }
        ],
        autoScroll: false,
        layout: 'fit',
        config: {
            defaultSettings: {
                groupByField: 'ScheduleState',
                showRows: false,
                columns: Ext.JSON.encode({
                    Defined: {wip: ''},
                    'In-Progress': {wip: ''},
                    Completed: {wip: ''},
                    Accepted: {wip: ''}
                }),
                cardFields: 'FormattedID,Name,Owner,Discussion,Tasks,Defects', //remove with COLUMN_LEVEL_FIELD_PICKER_ON_KANBAN_SETTINGS
                hideReleasedCards: false,
                showCardAge: true,
                cardAgeThreshold: 3,
                excludeWeekendsFromSLA: true,
                showSLA: false,
                pageSize: 25
            }
        },

        launch: function() {

            Rally.data.ModelFactory.getModel({
                type: 'UserStory',
                success: this._onStoryModelRetrieved,
                scope: this
            });
        },

        getOptions: function() {
            return [
                {
                    text: 'Show Cycle Time Report',
                    handler: this._showCycleTimeReport,
                    scope: this
                },
                {
                    text: 'Show Throughput Report',
                    handler: this._showThroughputReport,
                    scope: this
                },
                {
                    text: 'Print',
                    handler: this._print,
                    scope: this
                }
            ];
        },

        getSettingsFields: function() {
            return Rally.apps.kanban.Settings.getFields({
                shouldShowColumnLevelFieldPicker: this._shouldShowColumnLevelFieldPicker(),
                defaultCardFields: this.getSetting('cardFields')
            });
        },

        /**
         * Called when any timebox scope change is received.
         * @protected
         * @param {Rally.app.TimeboxScope} timeboxScope The new scope
         */
        onTimeboxScopeChange: function(timeboxScope) {
            this.callParent(arguments);
            this.gridboard.destroy();
            this.launch();
        },

        _shouldShowSwimLanes: function() {
            return true;//this.getContext().isFeatureEnabled('F5684_KANBAN_SWIM_LANES');
        },

        _shouldShowColumnLevelFieldPicker: function() {
            return this.getContext().isFeatureEnabled('COLUMN_LEVEL_FIELD_PICKER_ON_KANBAN_SETTINGS');
        },

        _onStoryModelRetrieved: function(model) {
            this.groupByField = model.getField(this.getSetting('groupByField'));
            this._addCardboardContent();
        },

        _addCardboardContent: function() {
            var cardboardConfig = this._getCardboardConfig();

            var columnSetting = this._getColumnSetting();
            if (columnSetting) {
                cardboardConfig.columns = this._getColumnConfig(columnSetting);
            }

			//show team SLA on board
			if (this.getSetting('sla') && this.getSetting('showSLA')) {
				var slaPanel = Ext.create('Ext.form.Panel', {
					items: [{
						xtype: 'label',
						text: 'Team Service Level Agreement: ' + this.getSetting('sla') + ' days',
						cls: 'sla'
					}]
				});

				this.down('#slaContainer').add(slaPanel);
			}

			this.gridboard = this.down('#bodyContainer').add(this._getGridboardConfig(cardboardConfig));

            this.cardboard = this.gridboard.getGridOrBoard();
        },

        _getGridboardConfig: function(cardboardConfig) {
            var context = this.getContext(),
                modelNames = this._getDefaultTypes();
            return {
                xtype: 'rallygridboard',
                stateful: false,
                toggleState: 'board',
                cardBoardConfig: cardboardConfig,
                shouldDestroyTreeStore: this.getContext().isFeatureEnabled('S73617_GRIDBOARD_SHOULD_DESTROY_TREESTORE'),
                plugins: [
                    'rallygridboardaddnew',
                    {
                        ptype: 'rallygridboardcustomfiltercontrol',
                        filterChildren: true,
                        filterControlConfig: {
                            blackListFields: [],
                            whiteListFields: [],
                            context: context,
                            margin: '3 9 3 30',
                            modelNames: modelNames,
                            stateful: true,
                            stateId: context.getScopedStateId('kanban-custom-filter-button')
                        }
                    },
                    {
                        ptype: 'rallygridboardfieldpicker',
                        headerPosition: 'left',
                        boardFieldBlackList: ['Successors', 'Predecessors', 'DisplayColor'],
                        modelNames: modelNames,
                        boardFieldDefaults: this.getSetting('cardFields').split(',')
                    },

                    {
                        ptype: 'rallyboardpolicydisplayable',
                        prefKey: 'kanbanAgreementsChecked',
                        checkboxConfig: {
                            boxLabel: 'Show Agreements'
                        }
                    }
                ],
                context: context,
                modelNames: modelNames,
                addNewPluginConfig: {
                    listeners: {
                        beforecreate: this._onBeforeCreate,
                        beforeeditorshow: this._onBeforeEditorShow,
                        scope: this
                    },
                    style: {
                        'float': 'left'
                    }
                },
                storeConfig: {
                    filters: this._getFilters()
                },
                height: this.getHeight()
            };
        },

        _getColumnConfig: function(columnSetting) {
            var columns = [];
            Ext.Object.each(columnSetting, function(column, values) {
                var columnConfig = {
                    xtype: 'kanbancolumn',
                    enableWipLimit: true,
                    wipLimit: values.wip,
                    plugins: [{
                        ptype: 'rallycolumnpolicy',
                        app: this
                    }],

                    value: column,
                    columnHeaderConfig: {
                        headerTpl: column || 'None'
                    },

                    listeners: {
                        invalidfilter: {
                            fn: this._onInvalidFilter,
                            scope: this
                        }
                    }
                };
                if(this._shouldShowColumnLevelFieldPicker()) {
                    columnConfig.fields = this._getFieldsForColumn(values);
                }
                columns.push(columnConfig);
            }, this);

            columns[columns.length - 1].storeConfig = {
                filters: this._getLastColumnFilter()
            };

            return columns;
        },

        _getFieldsForColumn: function(values) {
            var columnFields = [];
            if (this._shouldShowColumnLevelFieldPicker()) {
                if (values.cardFields) {
                    columnFields = values.cardFields.split(',');
                } else if (this.getSetting('cardFields')) {
                    columnFields = this.getSetting('cardFields').split(',');
                }
            }
            return columnFields;
        },

        _onInvalidFilter: function() {
            Rally.ui.notify.Notifier.showError({
                message: 'Invalid query: ' + this.getSetting('query')
            });
        },

        _getCardboardConfig: function() {
            var config = {
                xtype: 'rallycardboard',
                plugins: [
                    {ptype: 'rallycardboardprinting', pluginId: 'print'},
                    {
                        ptype: 'rallyscrollablecardboard',
                        containerEl: this.getEl()
                    },
                    {ptype: 'rallyfixedheadercardboard'}
                ],
                types: this._getDefaultTypes(),
                attribute: this.getSetting('groupByField'),
                margin: '10px',
                context: this.getContext(),
                listeners: {
                    beforecarddroppedsave: this._onBeforeCardSaved,
                    load: this._onBoardLoad,
                    cardupdated: this._publishContentUpdatedNoDashboardLayout,
                    scope: this
                },
                columnConfig: {
                    xtype: 'rallycardboardcolumn',
                    enableWipLimit: true
                },
                cardConfig: {
                    editable: true,
                    showIconMenus: true,
                    showAge: this.getSetting('showCardAge') ? this.getSetting('cardAgeThreshold') : -1,
                    showBlockedReason: true,
                    listeners: {
                        afterrender: this._showCardSLA,
                        rerender: this._showCardSLA,
                        scope: this

                    }
                },

                storeConfig: {
                    context: this.getContext().getDataContext(),
                    fetch: ["InProgressDate", "ScheduleState"]
                }
            };
            if (this.getSetting('showRows')) {
                Ext.merge(config, {
                    rowConfig: {
                        field: this.getSetting('rowsField'),
                        sortDirection: 'ASC'
                    }
                });
            }
            return config;
        },

		//Show Service Level Agreement status for a card (if configured to show)
		_showCardSLA: function(card) {
			var inProgressDate, daysInProgress, serviceLevelAgreement, remainingDaysForSLA, toolTipText, slaText, today;

			serviceLevelAgreement = this.getSetting('sla');

			//SLA has not been set
			if (!serviceLevelAgreement) {
				return;
			}

			inProgressDate = card.record.get('InProgressDate');
			today = new Date();

			//if the card is not in progress or is done, don't calculate SLA
			if (!inProgressDate || (card.record.get('ScheduleState') === 'Accepted')) {
				return;
			}

			//get number of days in progress
			daysInProgress = this._getNumberOfWorkDaysWithinRange(inProgressDate, today);

			//if we have a service level agreement that is at least 0, calculate SLA
			if (serviceLevelAgreement >= 0) {
				var statusContent, statusField, content, statusValue;

				statusContent = card.el.query('.status-content')[0];
				statusField = document.createElement('div');
				statusField.className = 'field-content status-field';

				statusValue = document.createElement('div');
				statusValue.className = 'status-value age';
				//statusValue.title = toolTipText;

				content = document.createElement('span');

				remainingDaysForSLA = (serviceLevelAgreement - daysInProgress);

				//no more days remaining
				if (remainingDaysForSLA === 0) {
					slaText = 'Due Today';
					//toolTipText = slaText + '
 In Progress for'  + daysInProgress + ' days.
 Started: ' + inProgressDate;
				}
				//violating the SLA
				else if (remainingDaysForSLA < 0) {
					slaText = 'Past Due by ' + Math.abs(remainingDaysForSLA) + ' days';
					statusValue.className = 'status-value age sla-violation';
					//toolTipText = slaText + '
 In Progress for ' + daysInProgress + ' days.
 Started: ' + inProgressDate;
				}
				//days left before SLA
				else {
					slaText = 'Due in ' + remainingDaysForSLA + ' days';
					//toolTipText = slaText + '
 In Progress for ' + daysInProgress + ' days.
 Started: ' + inProgressDate;
				}
				content.appendChild(document.createTextNode(slaText));

				statusValue.appendChild(content);
				statusField.appendChild(statusValue);
				statusContent.appendChild(statusField);
			}
        },

		//calculate the number of days in progress; if excluding weekends, only Mon-Fri will be included in the calculation
		_getNumberOfWorkDaysWithinRange: function(startDate, endDate) {
			var numberOfDays = 0, includeWeekends = this.getSetting('excludeWeekendsFromSLA') === false;
			var start = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
            var end = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());

			while (start < end) {
				if (includeWeekends || (start.getDay() > 0 && start.getDay() < 6)) {
					numberOfDays++;
				}
				start.setDate(start.getDate()+1);
			}

			return numberOfDays;
		},

        _getFilters: function() {
            var filters = [];
            if(this.getSetting('query')) {
                filters.push(Rally.data.QueryFilter.fromQueryString(this.getSetting('query')));
            }
            if(this.getContext().getTimeboxScope()) {
                filters.push(this.getContext().getTimeboxScope().getQueryFilter());
            }
            return filters;
        },

        _getLastColumnFilter: function() {
            return this.getSetting('hideReleasedCards') ?
                [
                    {
                        property: 'Release',
                        value: null
                    }
                ] : [];
        },

        _getColumnSetting: function() {
            var columnSetting = this.getSetting('columns');
            return columnSetting && Ext.JSON.decode(columnSetting);
        },

        _buildReportConfig: function(report) {

            var reportConfig = {
                report: report,
                work_items: this._getWorkItemTypesForChart()
            };
            if (this.getSetting('groupByField') !== 'ScheduleState') {
                reportConfig.filter_field = this.groupByField.displayName;
            }
            return reportConfig;
        },

        _showCycleTimeReport: function() {
            this._showReportDialog('Cycle Time Report',
                this._buildReportConfig(Rally.ui.report.StandardReport.Reports.CycleLeadTime));
        },

        _showThroughputReport: function() {
            this._showReportDialog('Throughput Report',
                this._buildReportConfig(Rally.ui.report.StandardReport.Reports.Throughput));
        },

        _print: function() {
            this.gridboard.getGridOrBoard().openPrintPage({title: 'Kanban Board'});
        },

        _getWorkItemTypesForChart: function() {
            var types = this.gridboard.getGridOrBoard().getTypes(),
                typeMap = {
                    hierarchicalrequirement: 'G',
                    defect: 'D'
                };
            return types.length === 2 ? 'N' : typeMap[types[0]];
        },

        _getDefaultTypes: function() {
            return ['User Story', 'Defect'];
        },

        _buildStandardReportConfig: function(reportConfig) {
            var scope = this.getContext().getDataContext();
            return {
                xtype: 'rallystandardreport',
                padding: 10,
                project: scope.project,
                projectScopeUp: scope.projectScopeUp,
                projectScopeDown: scope.projectScopeDown,
                reportConfig: reportConfig
            };
        },

        _showReportDialog: function(title, reportConfig) {
            var height = 450, width = 600;
            this.getEl().mask();
            Ext.create('Rally.ui.dialog.Dialog', {
                title: title,
                autoShow: true,
                draggable: false,
                closable: true,
                modal: false,
                height: height,
                width: width,
                items: [
                    Ext.apply(this._buildStandardReportConfig(reportConfig),
                        {
                            height: height,
                            width: width
                        })
                ],
                listeners: {
                    close: function() {
                        this.getEl().unmask();
                    },
                    scope: this
                }
            });
        },

        _onBoardLoad: function() {
            this._publishContentUpdated();
            this.setLoading(false);

        },

        _onBeforeCreate: function(addNew, record, params) {
            Ext.apply(params, {
                rankTo: 'BOTTOM',
                rankScope: 'BACKLOG'
            });
            record.set(this.getSetting('groupByField'), this.gridboard.getGridOrBoard().getColumns()[0].getValue());
        },

        _onBeforeEditorShow: function(addNew, params) {
            params.rankTo = 'BOTTOM';
            params.rankScope = 'BACKLOG';

            var groupByFieldName = this.groupByField.name;

            params[groupByFieldName] = this.gridboard.getGridOrBoard().getColumns()[0].getValue();
        },

        _onBeforeCardSaved: function(column, card, type) {
            var columnSetting = this._getColumnSetting();
            if (columnSetting) {
                var setting = columnSetting[column.getValue()];
                if (setting && setting.scheduleStateMapping) {
                    card.getRecord().set('ScheduleState', setting.scheduleStateMapping);
                }
            }
        },

        _publishContentUpdated: function() {
            this.fireEvent('contentupdated');
            if (Rally.BrowserTest) {
                Rally.BrowserTest.publishComponentReady(this);
            }
            this.recordComponentReady({
                miscData: {
                    swimLanes: this.getSetting('showRows'),
                    swimLaneField: this.getSetting('rowsField')
                }
            });
        },

        _publishContentUpdatedNoDashboardLayout: function() {
            this.fireEvent('contentupdated', {dashboardLayout: false});

        }
    });
})();
<commit_msg>Fix merge issue with fixed column headers<commit_after>(function() {
    var Ext = window.Ext4 || window.Ext;

    Ext.define('Rally.apps.kanban.KanbanApp', {
        extend: 'Rally.app.App',
        requires: [
            'Rally.apps.kanban.Settings',
            'Rally.apps.kanban.Column',
            'Rally.ui.gridboard.GridBoard',
            'Rally.ui.gridboard.plugin.GridBoardAddNew',

            'Rally.ui.gridboard.plugin.BoardPolicyDisplayable',
            'Rally.ui.cardboard.plugin.ColumnPolicy',
            'Rally.ui.cardboard.PolicyContainer',
            'Rally.ui.cardboard.CardBoard',
            'Rally.ui.cardboard.plugin.Scrollable',
            'Rally.ui.report.StandardReport',
            'Rally.clientmetrics.ClientMetricsRecordable',
            'Rally.ui.gridboard.plugin.GridBoardCustomFilterControl',
            'Rally.ui.gridboard.plugin.GridBoardFieldPicker',
            'Rally.ui.cardboard.plugin.FixedHeader'
        ],
        mixins: [
            'Rally.clientmetrics.ClientMetricsRecordable'
        ],
        cls: 'kanban',
        alias: 'widget.kanbanapp',
        appName: 'Kanban',

        settingsScope: 'project',

        items: [
			{
				xtype: 'container',
				itemId: 'slaContainer'
			},
            {
                xtype: 'container',
                itemId: 'bodyContainer'
            }
        ],
        //autoScroll: false,
        //layout: 'fit',
        config: {
            defaultSettings: {
                groupByField: 'ScheduleState',
                showRows: false,
                columns: Ext.JSON.encode({
                    Defined: {wip: ''},
                    'In-Progress': {wip: ''},
                    Completed: {wip: ''},
                    Accepted: {wip: ''}
                }),
                cardFields: 'FormattedID,Name,Owner,Discussion,Tasks,Defects', //remove with COLUMN_LEVEL_FIELD_PICKER_ON_KANBAN_SETTINGS
                hideReleasedCards: false,
                showCardAge: true,
                cardAgeThreshold: 3,
                excludeWeekendsFromSLA: true,
                showSLA: false,
                pageSize: 25
            }
        },

        launch: function() {

            Rally.data.ModelFactory.getModel({
                type: 'UserStory',
                success: this._onStoryModelRetrieved,
                scope: this
            });
        },

        getOptions: function() {
            return [
                {
                    text: 'Show Cycle Time Report',
                    handler: this._showCycleTimeReport,
                    scope: this
                },
                {
                    text: 'Show Throughput Report',
                    handler: this._showThroughputReport,
                    scope: this
                },
                {
                    text: 'Print',
                    handler: this._print,
                    scope: this
                }
            ];
        },

        getSettingsFields: function() {
            return Rally.apps.kanban.Settings.getFields({
                shouldShowColumnLevelFieldPicker: this._shouldShowColumnLevelFieldPicker(),
                defaultCardFields: this.getSetting('cardFields')
            });
        },

        /**
         * Called when any timebox scope change is received.
         * @protected
         * @param {Rally.app.TimeboxScope} timeboxScope The new scope
         */
        onTimeboxScopeChange: function(timeboxScope) {
            this.callParent(arguments);
            this.gridboard.destroy();
            this.launch();
        },

        _shouldShowSwimLanes: function() {
            return true;//this.getContext().isFeatureEnabled('F5684_KANBAN_SWIM_LANES');
        },

        _shouldShowColumnLevelFieldPicker: function() {
            return this.getContext().isFeatureEnabled('COLUMN_LEVEL_FIELD_PICKER_ON_KANBAN_SETTINGS');
        },

        _onStoryModelRetrieved: function(model) {
            this.groupByField = model.getField(this.getSetting('groupByField'));
            this._addCardboardContent();
        },

        _addCardboardContent: function() {
            var cardboardConfig = this._getCardboardConfig();

            var columnSetting = this._getColumnSetting();
            if (columnSetting) {
                cardboardConfig.columns = this._getColumnConfig(columnSetting);
            }

			//show team SLA on board
			if (this.getSetting('sla') && this.getSetting('showSLA')) {
				var slaPanel = Ext.create('Ext.form.Panel', {
					items: [{
						xtype: 'label',
						text: 'Team Service Level Agreement: ' + this.getSetting('sla') + ' days',
						cls: 'sla'
					}]
				});

				this.down('#slaContainer').add(slaPanel);
			}

			this.gridboard = this.down('#bodyContainer').add(this._getGridboardConfig(cardboardConfig));

            this.cardboard = this.gridboard.getGridOrBoard();
        },

        _getGridboardConfig: function(cardboardConfig) {
            var context = this.getContext(),
                modelNames = this._getDefaultTypes();
            return {
                xtype: 'rallygridboard',
                stateful: false,
                toggleState: 'board',
                cardBoardConfig: cardboardConfig,
                shouldDestroyTreeStore: this.getContext().isFeatureEnabled('S73617_GRIDBOARD_SHOULD_DESTROY_TREESTORE'),
                plugins: [
                    'rallygridboardaddnew',
                    {
                        ptype: 'rallygridboardcustomfiltercontrol',
                        filterChildren: true,
                        filterControlConfig: {
                            blackListFields: [],
                            whiteListFields: [],
                            context: context,
                            margin: '3 9 3 30',
                            modelNames: modelNames,
                            stateful: true,
                            stateId: context.getScopedStateId('kanban-custom-filter-button')
                        }
                    },
                    {
                        ptype: 'rallygridboardfieldpicker',
                        headerPosition: 'left',
                        boardFieldBlackList: ['Successors', 'Predecessors', 'DisplayColor'],
                        modelNames: modelNames,
                        boardFieldDefaults: this.getSetting('cardFields').split(',')
                    },

                    {
                        ptype: 'rallyboardpolicydisplayable',
                        prefKey: 'kanbanAgreementsChecked',
                        checkboxConfig: {
                            boxLabel: 'Show Agreements'
                        }
                    }
                ],
                context: context,
                modelNames: modelNames,
                addNewPluginConfig: {
                    listeners: {
                        beforecreate: this._onBeforeCreate,
                        beforeeditorshow: this._onBeforeEditorShow,
                        scope: this
                    },
                    style: {
                        'float': 'left'
                    }
                },
                storeConfig: {
                    filters: this._getFilters()
                },
                height: this.getHeight()
            };
        },

        _getColumnConfig: function(columnSetting) {
            var columns = [];
            Ext.Object.each(columnSetting, function(column, values) {
                var columnConfig = {
                    xtype: 'kanbancolumn',
                    enableWipLimit: true,
                    wipLimit: values.wip,
                    plugins: [{
                        ptype: 'rallycolumnpolicy',
                        app: this
                    }],

                    value: column,
                    columnHeaderConfig: {
                        headerTpl: column || 'None'
                    },

                    listeners: {
                        invalidfilter: {
                            fn: this._onInvalidFilter,
                            scope: this
                        }
                    }
                };
                if(this._shouldShowColumnLevelFieldPicker()) {
                    columnConfig.fields = this._getFieldsForColumn(values);
                }
                columns.push(columnConfig);
            }, this);

            columns[columns.length - 1].storeConfig = {
                filters: this._getLastColumnFilter()
            };

            return columns;
        },

        _getFieldsForColumn: function(values) {
            var columnFields = [];
            if (this._shouldShowColumnLevelFieldPicker()) {
                if (values.cardFields) {
                    columnFields = values.cardFields.split(',');
                } else if (this.getSetting('cardFields')) {
                    columnFields = this.getSetting('cardFields').split(',');
                }
            }
            return columnFields;
        },

        _onInvalidFilter: function() {
            Rally.ui.notify.Notifier.showError({
                message: 'Invalid query: ' + this.getSetting('query')
            });
        },

        _getCardboardConfig: function() {
            var config = {
                xtype: 'rallycardboard',
                plugins: [
                    {ptype: 'rallycardboardprinting', pluginId: 'print'},
                    {
                        ptype: 'rallyscrollablecardboard',
                        containerEl: this.getEl()
                    },
                    {ptype: 'rallyfixedheadercardboard'}
                ],
                types: this._getDefaultTypes(),
                attribute: this.getSetting('groupByField'),
                margin: '10px',
                context: this.getContext(),
                listeners: {
                    beforecarddroppedsave: this._onBeforeCardSaved,
                    load: this._onBoardLoad,
                    cardupdated: this._publishContentUpdatedNoDashboardLayout,
                    scope: this
                },
                columnConfig: {
                    xtype: 'rallycardboardcolumn',
                    enableWipLimit: true
                },
                cardConfig: {
                    editable: true,
                    showIconMenus: true,
                    showAge: this.getSetting('showCardAge') ? this.getSetting('cardAgeThreshold') : -1,
                    showBlockedReason: true,
                    listeners: {
                        afterrender: this._showCardSLA,
                        rerender: this._showCardSLA,
                        scope: this

                    }
                },

                storeConfig: {
                    context: this.getContext().getDataContext(),
                    fetch: ["InProgressDate", "ScheduleState"]
                }
            };
            if (this.getSetting('showRows')) {
                Ext.merge(config, {
                    rowConfig: {
                        field: this.getSetting('rowsField'),
                        sortDirection: 'ASC'
                    }
                });
            }
            return config;
        },

		//Show Service Level Agreement status for a card (if configured to show)
		_showCardSLA: function(card) {
			var inProgressDate, daysInProgress, serviceLevelAgreement, remainingDaysForSLA, toolTipText, slaText, today;

			serviceLevelAgreement = this.getSetting('sla');

			//SLA has not been set
			if (!serviceLevelAgreement) {
				return;
			}

			inProgressDate = card.record.get('InProgressDate');
			today = new Date();

			//if the card is not in progress or is done, don't calculate SLA
			if (!inProgressDate || (card.record.get('ScheduleState') === 'Accepted')) {
				return;
			}

			//get number of days in progress
			daysInProgress = this._getNumberOfWorkDaysWithinRange(inProgressDate, today);

			//if we have a service level agreement that is at least 0, calculate SLA
			if (serviceLevelAgreement >= 0) {
				var statusContent, statusField, content, statusValue;

				statusContent = card.el.query('.status-content')[0];
				statusField = document.createElement('div');
				statusField.className = 'field-content status-field';

				statusValue = document.createElement('div');
				statusValue.className = 'status-value age';
				//statusValue.title = toolTipText;

				content = document.createElement('span');

				remainingDaysForSLA = (serviceLevelAgreement - daysInProgress);

				//no more days remaining
				if (remainingDaysForSLA === 0) {
					slaText = 'Due Today';
					//toolTipText = slaText + '
 In Progress for'  + daysInProgress + ' days.
 Started: ' + inProgressDate;
				}
				//violating the SLA
				else if (remainingDaysForSLA < 0) {
					slaText = 'Past Due by ' + Math.abs(remainingDaysForSLA) + ' days';
					statusValue.className = 'status-value age sla-violation';
					//toolTipText = slaText + '
 In Progress for ' + daysInProgress + ' days.
 Started: ' + inProgressDate;
				}
				//days left before SLA
				else {
					slaText = 'Due in ' + remainingDaysForSLA + ' days';
					//toolTipText = slaText + '
 In Progress for ' + daysInProgress + ' days.
 Started: ' + inProgressDate;
				}
				content.appendChild(document.createTextNode(slaText));

				statusValue.appendChild(content);
				statusField.appendChild(statusValue);
				statusContent.appendChild(statusField);
			}
        },

		//calculate the number of days in progress; if excluding weekends, only Mon-Fri will be included in the calculation
		_getNumberOfWorkDaysWithinRange: function(startDate, endDate) {
			var numberOfDays = 0, includeWeekends = this.getSetting('excludeWeekendsFromSLA') === false;
			var start = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
            var end = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());

			while (start < end) {
				if (includeWeekends || (start.getDay() > 0 && start.getDay() < 6)) {
					numberOfDays++;
				}
				start.setDate(start.getDate()+1);
			}

			return numberOfDays;
		},

        _getFilters: function() {
            var filters = [];
            if(this.getSetting('query')) {
                filters.push(Rally.data.QueryFilter.fromQueryString(this.getSetting('query')));
            }
            if(this.getContext().getTimeboxScope()) {
                filters.push(this.getContext().getTimeboxScope().getQueryFilter());
            }
            return filters;
        },

        _getLastColumnFilter: function() {
            return this.getSetting('hideReleasedCards') ?
                [
                    {
                        property: 'Release',
                        value: null
                    }
                ] : [];
        },

        _getColumnSetting: function() {
            var columnSetting = this.getSetting('columns');
            return columnSetting && Ext.JSON.decode(columnSetting);
        },

        _buildReportConfig: function(report) {

            var reportConfig = {
                report: report,
                work_items: this._getWorkItemTypesForChart()
            };
            if (this.getSetting('groupByField') !== 'ScheduleState') {
                reportConfig.filter_field = this.groupByField.displayName;
            }
            return reportConfig;
        },

        _showCycleTimeReport: function() {
            this._showReportDialog('Cycle Time Report',
                this._buildReportConfig(Rally.ui.report.StandardReport.Reports.CycleLeadTime));
        },

        _showThroughputReport: function() {
            this._showReportDialog('Throughput Report',
                this._buildReportConfig(Rally.ui.report.StandardReport.Reports.Throughput));
        },

        _print: function() {
            this.gridboard.getGridOrBoard().openPrintPage({title: 'Kanban Board'});
        },

        _getWorkItemTypesForChart: function() {
            var types = this.gridboard.getGridOrBoard().getTypes(),
                typeMap = {
                    hierarchicalrequirement: 'G',
                    defect: 'D'
                };
            return types.length === 2 ? 'N' : typeMap[types[0]];
        },

        _getDefaultTypes: function() {
            return ['User Story', 'Defect'];
        },

        _buildStandardReportConfig: function(reportConfig) {
            var scope = this.getContext().getDataContext();
            return {
                xtype: 'rallystandardreport',
                padding: 10,
                project: scope.project,
                projectScopeUp: scope.projectScopeUp,
                projectScopeDown: scope.projectScopeDown,
                reportConfig: reportConfig
            };
        },

        _showReportDialog: function(title, reportConfig) {
            var height = 450, width = 600;
            this.getEl().mask();
            Ext.create('Rally.ui.dialog.Dialog', {
                title: title,
                autoShow: true,
                draggable: false,
                closable: true,
                modal: false,
                height: height,
                width: width,
                items: [
                    Ext.apply(this._buildStandardReportConfig(reportConfig),
                        {
                            height: height,
                            width: width
                        })
                ],
                listeners: {
                    close: function() {
                        this.getEl().unmask();
                    },
                    scope: this
                }
            });
        },

        _onBoardLoad: function() {
            this._publishContentUpdated();
            this.setLoading(false);

        },

        _onBeforeCreate: function(addNew, record, params) {
            Ext.apply(params, {
                rankTo: 'BOTTOM',
                rankScope: 'BACKLOG'
            });
            record.set(this.getSetting('groupByField'), this.gridboard.getGridOrBoard().getColumns()[0].getValue());
        },

        _onBeforeEditorShow: function(addNew, params) {
            params.rankTo = 'BOTTOM';
            params.rankScope = 'BACKLOG';

            var groupByFieldName = this.groupByField.name;

            params[groupByFieldName] = this.gridboard.getGridOrBoard().getColumns()[0].getValue();
        },

        _onBeforeCardSaved: function(column, card, type) {
            var columnSetting = this._getColumnSetting();
            if (columnSetting) {
                var setting = columnSetting[column.getValue()];
                if (setting && setting.scheduleStateMapping) {
                    card.getRecord().set('ScheduleState', setting.scheduleStateMapping);
                }
            }
        },

        _publishContentUpdated: function() {
            this.fireEvent('contentupdated');
            if (Rally.BrowserTest) {
                Rally.BrowserTest.publishComponentReady(this);
            }
            this.recordComponentReady({
                miscData: {
                    swimLanes: this.getSetting('showRows'),
                    swimLaneField: this.getSetting('rowsField')
                }
            });
        },

        _publishContentUpdatedNoDashboardLayout: function() {
            this.fireEvent('contentupdated', {dashboardLayout: false});

        }
    });
})();
<|endoftext|>


<commit_before><commit_msg>Update LMexpress.user.js<commit_after><|endoftext|>