<commit_before>// Some portions of this (around custom CF functions) are from:
// https://gist.github.com/fcheung/baec53381350a4b11037

var aws = require('aws-sdk');
var util = require('util');

exports.handler = function(event, context) {
    var args = getArgs(event);
    console.log("Handling '" + args.ResourceType + "' event '" + args.RequestType + "'");

    if (args.ResourceType === 'Custom::RemoteRoute53RecordSet') {
        handleRecordSet(args, context);
    } else {
        errMsg = "unknown resource type: " + args.ResourceType;
        console.log(errMsg);
        if (args.RequestType == 'Delete') {
            // Report success, because we can never delete the stack otherwise.
            response.send(args, context, response.SUCCESS, errMsg);
        } else {
            response.send(args, context, response.FAILED, errMsg);
        }
    }
};

var getArgs = function(event) {
    // Were we called by SNS?
    if (event.Records && event.Records.length > 0 &&
        event.Records[0].Sns) {
        return JSON.parse(event.Records[0].Sns.Message);
    } else {
        // Assume we were called directly
        return event;
    }
};

var handleRecordSet = function(event, context) {
    if (validateCombos(event, context)) { // This takes care of failing out if needed
        if (event.RequestType === 'Delete') {
            deleteRecordSet(event, context);
        } else if (event.RequestType === 'Create') {
            createRecordSet(event, context);
        } else if (event.RequestType === 'Update') {
            updateRecordSet(event, context);
        } else {
            errMsg = "unknown request type: " + event.RequestType;
            console.log(errMsg);
            response.send(event, context, response.FAILED, errMsg);
        }
    }
};

var createRecordSet = function(event, context) {
    withRemoteRoute53(event, context, function(event, context, route53) {
        withHostedZone(event, context, route53, function (event, context, route53, hostedZoneId) {
            var params = {
                ChangeBatch: {
                    Changes: [
                        {
                            Action: 'CREATE',
                            ResourceRecordSet: buildRecordSet(event)
                        }
                    ]
                },
                HostedZoneId: hostedZoneId
            };
            route53.changeResourceRecordSets(params, function (err, data) {
                if (err) {
                    errMsg = "create record set failed: " + err;
                    console.log(errMsg);
                    response.send(event, context, response.FAILED, errMsg);

                } else {
                    response.send(event, context, response.SUCCESS, null, {}, physicalId(event.ResourceProperties));
                }
            });
        });
    });
};

var updateRecordSet = function(event, context) {
    withRemoteRoute53(event, context, function(event, context, route53) {
        withHostedZone(event, context, route53, function (event, context, route53, hostedZoneId) {
            var params = {
                ChangeBatch: {
                    Changes: [
                        {
                            Action: 'UPSERT',
                            ResourceRecordSet: buildRecordSet(event)
                        }
                    ]
                },
                HostedZoneId: hostedZoneId
            };
            route53.changeResourceRecordSets(params, function (err, data) {
                if (err) {
                    errMsg = "update record set failed: " + err;
                    console.log(errMsg);
                    response.send(event, context, response.FAILED, errMsg);

                } else {
                    response.send(event, context, response.SUCCESS, null, {}, physicalId(event.ResourceProperties));
                }
            });
        });
    });
};

var deleteRecordSet = function(event, context) {
    if(event.PhysicalResourceId.match(/^route53-record-set-/)){
        withRemoteRoute53(event, context, function(event, context, route53) {
            withHostedZone(event, context, route53, function (event, context, route53, hostedZoneId) {
                var params = {
                    ChangeBatch: {
                        Changes: [
                            {
                                Action: 'DELETE',
                                ResourceRecordSet: buildRecordSet(event)
                            }
                        ]
                    },
                    HostedZoneId: hostedZoneId
                };
                route53.changeResourceRecordSets(params, function (err, data) {
                    if (err) {
                        if (err.code == "InvalidChangeBatch" && err.message && err.message.indexOf("not found") >= 0) {
                            errMsg = "WARNING: " + err;
                            console.log(errMsg);
                            response.send(event, context, response.SUCCESS, errMsg, {}, physicalId(event.ResourceProperties));
                        } else {
                            errMsg = "delete record set failed: " + err;
                            console.log(errMsg);
                            response.send(event, context, response.FAILED, errMsg);
                        }

                    } else {
                        response.send(event, context, response.SUCCESS, null, {}, physicalId(event.ResourceProperties));
                    }
                });
            });
        });
    } else {
        errMsg = "unexpected physical id for record set " + event.PhysicalResourceId + " - ignoring";
        console.log(errMsg);
        response.send(event, context, response.SUCCESS, errMsg);
    }
};

var validateCombos = function(event, context) {
    if (!event.ResourceProperties.Name) {
        errMsg = "missing parameter Name";
        console.log(errMsg);
        response.send(event, context, response.FAILED, errMsg);
        return;
    }

    if (!event.ResourceProperties.Type) {
        errMsg = "missing parameter Type";
        console.log(errMsg);
        response.send(event, context, response.FAILED, errMsg);
        return;
    }

    if (!event.ResourceProperties.AliasTarget &&
            (!event.ResourceProperties.ResourceRecords || event.ResourceProperties.ResourceRecords.length == 0)) {
        errMsg = "Must specify ResourceRecords or AliasTarget.";
        console.log(errMsg);
        response.send(event, context, response.FAILED, errMsg);
        return false;
    }

    if (event.ResourceProperties.AliasTarget && event.ResourceProperties.TTL) {
        errMsg = "TTL not supported for Alias records.";
        console.log(errMsg);
        response.send(event, context, response.FAILED, errMsg);
        return false;
    }

    if (!event.ResourceProperties.AliasTarget && !event.ResourceProperties.TTL) {
        errMsg = "TTL is required for ResourceRecord records.";
        console.log(errMsg);
        response.send(event, context, response.FAILED, errMsg);
        return false;
    }

    if ((event.ResourceProperties.Type == 'CNAME' || event.ResourceProperties.Type == 'SOA')
        && event.ResourceProperties.ResourceRecords.length > 1) {
        errMsg = "Only one resource record allowed for CNAME or SOA records.";
        console.log(errMsg);
        response.send(event, context, response.FAILED, errMsg);
        return false;
    }
    if (!event.ResourceProperties.SetIdentifier && (
            event.ResourceProperties.Weight ||
            event.ResourceProperties.Region ||
            event.ResourceProperties.GeoLocation ||
            event.ResourceProperties.Failover
        )) {
        errMsg = "SetIdentifier is required for weighted, latency, failover, or geolocation records.";
        console.log(errMsg);
        response.send(event, context, response.FAILED, errMsg);
        return false;
    }
    return true;
};

var buildRecordSet = function(event) {
    var record = {
        Name: event.ResourceProperties.Name,
        Type: event.ResourceProperties.Type
    };
    if (event.ResourceProperties.TTL) {
        if (typeof event.ResourceProperties.TTL === 'number') {
            record.TTL = event.ResourceProperties.TTL;
        } else {
            record.TTL = parseInt(event.ResourceProperties.TTL);
        }
    }

    if (event.ResourceProperties.SetIdentifier) {
        record.SetIdentifier = event.ResourceProperties.SetIdentifier;
    }

    if (event.ResourceProperties.Weight) {
        if (typeof event.ResourceProperties.Weight === 'number') {
            record.Weight = event.ResourceProperties.Weight;
        } else {
            record.Weight = parseInt(event.ResourceProperties.Weight);
        }
    }

    if (event.ResourceProperties.Region) {
        record.Region = event.ResourceProperties.Region;
    }

    if (event.ResourceProperties.GeoLocation) {
        record.GeoLocation = event.ResourceProperties.GeoLocation;
    }

    if (event.ResourceProperties.Failover) {
        record.Failover = event.ResourceProperties.Failover;
    }

    if (event.ResourceProperties.AliasTarget) {
        record.AliasTarget = event.ResourceProperties.AliasTarget;
        // Comes in from CF as a string, needs to be a boolean
        if (typeof record.AliasTarget.EvaluateTargetHealth === 'string') {
            record.AliasTarget.EvaluateTargetHealth = record.AliasTarget.EvaluateTargetHealth.toLowerCase()=='true'?true:false;
        }
    }

    if (event.ResourceProperties.HealthCheckId) {
        record.HealthCheckId = event.ResourceProperties.HealthCheckId;
    }

    var resourceRecords = event.ResourceProperties.ResourceRecords;
    if (resourceRecords && resourceRecords.length > 0) {
        record.ResourceRecords = resourceRecords.map(function (input) {
            return {"Value": input};
        })
    }
    return record;
};

var getRoute53 = function(auth) {
    return new aws.Route53({
        accessKeyId: auth.AccessKeyId,
        secretAccessKey: auth.SecretAccessKey,
        sessionToken: auth.SessionToken,
        region: 'us-east-1' // Route53 always runs against us-east-1
    });
};

var withRemoteRoute53 = function(event, context, nextFunc) {
    var params = {
        RoleArn: event.ResourceProperties.DestinationRole,
        RoleSessionName: event.RequestId
    };
    var sts = new aws.STS();
    sts.assumeRole(params, function(err, data) {
        if (err) {
            console.log(err, err.stack);
            response.send(event, context, response.FAILED, err);
        }
        else {
            nextFunc(event, context, getRoute53(data.Credentials));
        }
    });
};

var withHostedZone = function(event, context, route53, nextFunc) {
    if (event.ResourceProperties.HostedZoneId) {
        // Make sure it exists and we can see it.
        var params = {
            Id: event.ResourceProperties.HostedZoneId
        };
        route53.getHostedZone(params, function(err, data) {
            if (err) {
                console.log(err, err.stack);
                response.send(event, context, response.FAILED, err);
            }
            else {
                nextFunc(event, context, route53, event.ResourceProperties.HostedZoneId)
            }
        });
    } else if (event.ResourceProperties.HostedZoneName) {
        route53.listHostedZones({}, function(err, data) {
            if (err) {
                console.log(err, err.stack);
                response.send(event, context, response.FAILED, err);
            }
            else {
                searchHostedZoneNames(event, context, route53, null, null, nextFunc);
            }
        });
    } else {
        response.send(event, context, response.FAILED, "Must specify HostedZoneId or HostedZoneName.");
    }
};

var searchHostedZoneNames = function(event, context, route53, marker, prevMatchResults, nextFunc) {
    var params = {};
    if (marker) {
        params = {'Marker': marker};
    }
    var fqdnToFind = event.ResourceProperties.HostedZoneName.endsWith('.')?event.ResourceProperties.HostedZoneName:(event.ResourceProperties.HostedZoneName + '.')

    route53.listHostedZones(params, function(err, data) {
        if (err) {
            console.log(err, err.stack);
            response.send(event, context, response.FAILED, err);
        }
        else {
            var matchResults = data.HostedZones.map(function(obj){
                var fqdnToCheck = obj.Name.endsWith('.')?obj.Name:(obj.Name + '.');

                if (fqdnToCheck == fqdnToFind) {
                    return obj.Id;
                } else {
                    return null;
                }
            }).filter(function(obj) {return obj != undefined});
            var aggregateResults = prevMatchResults?prevMatchResults.concat(matchResults):matchResults

            if (data.IsTruncated == true) {
                searchHostedZoneNames(event, context, route53, data.NextMarker, aggregateResults, nextFunc);
            } else {
                if (aggregateResults.length == 0) {
                    response.send(event, context, response.FAILED, "No zone found for HostedZoneName: " + event.ResourceProperties.HostedZoneName);
                } else if (aggregateResults.length > 1) {
                    response.send(event, context, response.FAILED, "Multiple zones found for HostedZoneName: " + event.ResourceProperties.HostedZoneName + " -- specify HostedZoneId.");
                } else {
                    nextFunc(event, context, route53, aggregateResults[0]);
                }
            }
        }
    });
};

var physicalId = function(properties) {
    return 'route53-record-set-' + properties.Name;
};

var sendSignal = function(handle, context, data){
    var body = JSON.stringify(data);
    var https = require("https");
    var url = require("url");
    console.log("signal body:\n", body);

    var parsedUrl = url.parse(handle);
    var options = {
        hostname: parsedUrl.hostname,
        port: 443,
        path: parsedUrl.path,
        method: "PUT",
        headers: {
            "content-type": "",
            "content-length": body.length
        }
    };

    var request = https.request(options, function(response) {
        console.log("Status code: " + response.statusCode);
        console.log("Status message: " + response.statusMessage);
        context.done();
    });

    request.on("error", function(error) {
        console.log("sendSignal(..) failed executing https.request(..): " + error);
        context.done();
    });

    request.write(body);
    request.end();
};

if (!String.prototype.endsWith) {
    String.prototype.endsWith = function(searchString, position) {
        var subjectString = this.toString();
        if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
            position = subjectString.length;
        }
        position -= searchString.length;
        var lastIndex = subjectString.indexOf(searchString, position);
        return lastIndex !== -1 && lastIndex === position;
    };
}

/* The below section is adapted from the cfn-response module, as published at:

 http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html

 */

/* Copyright 2015 Amazon Web Services, Inc. or its affiliates. All Rights Reserved.
 This file is licensed to you under the AWS Customer Agreement (the "License").
 You may not use this file except in compliance with the License.
 A copy of the License is located at http://aws.amazon.com/agreement/.
 This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied.
 See the License for the specific language governing permissions and limitations under the License. */
var response = {};

response.SUCCESS = "SUCCESS";
response.FAILED = "FAILED";

response.send = function(event, context, responseStatus, responseReason, responseData, physicalResourceId, continueFuncton) {
    reason = "CloudWatch Log Stream: " + context.logGroupName + " -- " + context.logStreamName
    if (responseReason) {
        reason = responseReason + " - " + reason
    }

    if (!responseData) {
        responseData = {}
    }

    var responseBody = JSON.stringify({
        Status: responseStatus,
        Reason: reason,
        PhysicalResourceId: physicalResourceId || context.logStreamName,
        StackId: event.StackId,
        RequestId: event.RequestId,
        LogicalResourceId: event.LogicalResourceId,
        Data: responseData
    });

    console.log("Response body:\n", responseBody);

    var https = require("https");
    var url = require("url");

    var parsedUrl = url.parse(event.ResponseURL);
    var options = {
        hostname: parsedUrl.hostname,
        port: 443,
        path: parsedUrl.path,
        method: "PUT",
        headers: {
            "content-type": "",
            "content-length": responseBody.length
        }
    };

    var request = https.request(options, function(response) {
        console.log("Status code: " + response.statusCode);
        console.log("Status message: " + response.statusMessage);
        if(!continueFuncton){
            context.done();
        }
    });

    request.on("error", function(error) {
        console.log("send(..) failed executing https.request(..): " + error);
        context.done();
    });

    request.write(responseBody);
    request.end();
};
<commit_msg>Fix race conditions on route 53 with retry logic<commit_after>// Some portions of this (around custom CF functions) are from:
// https://gist.github.com/fcheung/baec53381350a4b11037

var aws = require('aws-sdk');
var util = require('util');

exports.handler = function(event, context) {
    var args = getArgs(event);
    console.log("Handling '" + args.ResourceType + "' event '" + args.RequestType + "'");

    if (args.ResourceType === 'Custom::RemoteRoute53RecordSet') {
        handleRecordSet(args, context);
    } else {
        errMsg = "unknown resource type: " + args.ResourceType;
        console.log(errMsg);
        if (args.RequestType == 'Delete') {
            // Report success, because we can never delete the stack otherwise.
            response.send(args, context, response.SUCCESS, errMsg);
        } else {
            response.send(args, context, response.FAILED, errMsg);
        }
    }
};

var getArgs = function(event) {
    // Were we called by SNS?
    if (event.Records && event.Records.length > 0 &&
        event.Records[0].Sns) {
        return JSON.parse(event.Records[0].Sns.Message);
    } else {
        // Assume we were called directly
        return event;
    }
};

var handleRecordSet = function(event, context) {
    if (validateCombos(event, context)) { // This takes care of failing out if needed
        if (event.RequestType === 'Delete') {
            deleteRecordSet(event, context);
        } else if (event.RequestType === 'Create') {
            createRecordSet(event, context);
        } else if (event.RequestType === 'Update') {
            updateRecordSet(event, context);
        } else {
            errMsg = "unknown request type: " + event.RequestType;
            console.log(errMsg);
            response.send(event, context, response.FAILED, errMsg);
        }
    }
};

var createRecordSet = function(event, context) {
    withRemoteRoute53(event, context, function(event, context, route53) {
        withHostedZone(event, context, route53, function (event, context, route53, hostedZoneId) {
            var params = {
                ChangeBatch: {
                    Changes: [
                        {
                            Action: 'CREATE',
                            ResourceRecordSet: buildRecordSet(event)
                        }
                    ]
                },
                HostedZoneId: hostedZoneId
            };
            changeResourceRecordSetsHelper(route53, params, function (err, data) {
                if (err) {
                    errMsg = "create record set failed: " + err;
                    console.log(errMsg);
                    response.send(event, context, response.FAILED, errMsg);

                } else {
                    response.send(event, context, response.SUCCESS, null, {}, physicalId(event.ResourceProperties));
                }
            });
        });
    });
};

var updateRecordSet = function(event, context) {
    withRemoteRoute53(event, context, function(event, context, route53) {
        withHostedZone(event, context, route53, function (event, context, route53, hostedZoneId) {
            var params = {
                ChangeBatch: {
                    Changes: [
                        {
                            Action: 'UPSERT',
                            ResourceRecordSet: buildRecordSet(event)
                        }
                    ]
                },
                HostedZoneId: hostedZoneId
            };
            changeResourceRecordSetsHelper(route53, params, function (err, data) {
                if (err) {
                    errMsg = "update record set failed: " + err;
                    console.log(errMsg);
                    response.send(event, context, response.FAILED, errMsg);

                } else {
                    response.send(event, context, response.SUCCESS, null, {}, physicalId(event.ResourceProperties));
                }
            });
        });
    });
};

var deleteRecordSet = function(event, context) {
    if(event.PhysicalResourceId.match(/^route53-record-set-/)){
        withRemoteRoute53(event, context, function(event, context, route53) {
            withHostedZone(event, context, route53, function (event, context, route53, hostedZoneId) {
                var params = {
                    ChangeBatch: {
                        Changes: [
                            {
                                Action: 'DELETE',
                                ResourceRecordSet: buildRecordSet(event)
                            }
                        ]
                    },
                    HostedZoneId: hostedZoneId
                };
                changeResourceRecordSetsHelper(route53, params, function (err, data) {
                    if (err) {
                        if (err.code == "InvalidChangeBatch" && err.message && err.message.indexOf("not found") >= 0) {
                            errMsg = "WARNING: " + err;
                            console.log(errMsg);
                            response.send(event, context, response.SUCCESS, errMsg, {}, physicalId(event.ResourceProperties));
                        } else {
                            errMsg = "delete record set failed: " + err;
                            console.log(errMsg);
                            response.send(event, context, response.FAILED, errMsg);
                        }

                    } else {
                        response.send(event, context, response.SUCCESS, null, {}, physicalId(event.ResourceProperties));
                    }
                });
            });
        });
    } else {
        errMsg = "unexpected physical id for record set " + event.PhysicalResourceId + " - ignoring";
        console.log(errMsg);
        response.send(event, context, response.SUCCESS, errMsg);
    }
};

var changeResourceRecordSetsHelper = function(route53, params, callback, iteration) {
    var iterationLimit = 100;
    if (!iteration) {
        iteration = 0;
    }
    route53.changeResourceRecordSets(params, function (err, data) {
        if (err && err.code == "PriorRequestNotComplete") {
            if (iteration < iterationLimit) {
                console.log("WARNING: Prior request not complete.  Retrying");
                changeResourceRecordSetsHelper(route53, params, callback, ++iteration, iterationLimit);
            } else {
                console.log("ERR: Prior request not complete, but retry limit hit.  Giving up.");
                callback(err, data)
            }
        } else {
            callback(err, data);
        }
    });
};

var validateCombos = function(event, context) {
    if (!event.ResourceProperties.Name) {
        errMsg = "missing parameter Name";
        console.log(errMsg);
        response.send(event, context, response.FAILED, errMsg);
        return;
    }

    if (!event.ResourceProperties.Type) {
        errMsg = "missing parameter Type";
        console.log(errMsg);
        response.send(event, context, response.FAILED, errMsg);
        return;
    }

    if (!event.ResourceProperties.AliasTarget &&
            (!event.ResourceProperties.ResourceRecords || event.ResourceProperties.ResourceRecords.length == 0)) {
        errMsg = "Must specify ResourceRecords or AliasTarget.";
        console.log(errMsg);
        response.send(event, context, response.FAILED, errMsg);
        return false;
    }

    if (event.ResourceProperties.AliasTarget && event.ResourceProperties.TTL) {
        errMsg = "TTL not supported for Alias records.";
        console.log(errMsg);
        response.send(event, context, response.FAILED, errMsg);
        return false;
    }

    if (!event.ResourceProperties.AliasTarget && !event.ResourceProperties.TTL) {
        errMsg = "TTL is required for ResourceRecord records.";
        console.log(errMsg);
        response.send(event, context, response.FAILED, errMsg);
        return false;
    }

    if ((event.ResourceProperties.Type == 'CNAME' || event.ResourceProperties.Type == 'SOA')
        && event.ResourceProperties.ResourceRecords.length > 1) {
        errMsg = "Only one resource record allowed for CNAME or SOA records.";
        console.log(errMsg);
        response.send(event, context, response.FAILED, errMsg);
        return false;
    }
    if (!event.ResourceProperties.SetIdentifier && (
            event.ResourceProperties.Weight ||
            event.ResourceProperties.Region ||
            event.ResourceProperties.GeoLocation ||
            event.ResourceProperties.Failover
        )) {
        errMsg = "SetIdentifier is required for weighted, latency, failover, or geolocation records.";
        console.log(errMsg);
        response.send(event, context, response.FAILED, errMsg);
        return false;
    }
    return true;
};

var buildRecordSet = function(event) {
    var record = {
        Name: event.ResourceProperties.Name,
        Type: event.ResourceProperties.Type
    };
    if (event.ResourceProperties.TTL) {
        if (typeof event.ResourceProperties.TTL === 'number') {
            record.TTL = event.ResourceProperties.TTL;
        } else {
            record.TTL = parseInt(event.ResourceProperties.TTL);
        }
    }

    if (event.ResourceProperties.SetIdentifier) {
        record.SetIdentifier = event.ResourceProperties.SetIdentifier;
    }

    if (event.ResourceProperties.Weight) {
        if (typeof event.ResourceProperties.Weight === 'number') {
            record.Weight = event.ResourceProperties.Weight;
        } else {
            record.Weight = parseInt(event.ResourceProperties.Weight);
        }
    }

    if (event.ResourceProperties.Region) {
        record.Region = event.ResourceProperties.Region;
    }

    if (event.ResourceProperties.GeoLocation) {
        record.GeoLocation = event.ResourceProperties.GeoLocation;
    }

    if (event.ResourceProperties.Failover) {
        record.Failover = event.ResourceProperties.Failover;
    }

    if (event.ResourceProperties.AliasTarget) {
        record.AliasTarget = event.ResourceProperties.AliasTarget;
        // Comes in from CF as a string, needs to be a boolean
        if (typeof record.AliasTarget.EvaluateTargetHealth === 'string') {
            record.AliasTarget.EvaluateTargetHealth = record.AliasTarget.EvaluateTargetHealth.toLowerCase()=='true'?true:false;
        }
    }

    if (event.ResourceProperties.HealthCheckId) {
        record.HealthCheckId = event.ResourceProperties.HealthCheckId;
    }

    var resourceRecords = event.ResourceProperties.ResourceRecords;
    if (resourceRecords && resourceRecords.length > 0) {
        record.ResourceRecords = resourceRecords.map(function (input) {
            return {"Value": input};
        })
    }
    return record;
};

var getRoute53 = function(auth) {
    return new aws.Route53({
        accessKeyId: auth.AccessKeyId,
        secretAccessKey: auth.SecretAccessKey,
        sessionToken: auth.SessionToken,
        region: 'us-east-1' // Route53 always runs against us-east-1
    });
};

var withRemoteRoute53 = function(event, context, nextFunc) {
    var params = {
        RoleArn: event.ResourceProperties.DestinationRole,
        RoleSessionName: event.RequestId
    };
    var sts = new aws.STS();
    sts.assumeRole(params, function(err, data) {
        if (err) {
            console.log(err, err.stack);
            response.send(event, context, response.FAILED, err);
        }
        else {
            nextFunc(event, context, getRoute53(data.Credentials));
        }
    });
};

var withHostedZone = function(event, context, route53, nextFunc) {
    if (event.ResourceProperties.HostedZoneId) {
        // Make sure it exists and we can see it.
        var params = {
            Id: event.ResourceProperties.HostedZoneId
        };
        route53.getHostedZone(params, function(err, data) {
            if (err) {
                console.log(err, err.stack);
                response.send(event, context, response.FAILED, err);
            }
            else {
                nextFunc(event, context, route53, event.ResourceProperties.HostedZoneId)
            }
        });
    } else if (event.ResourceProperties.HostedZoneName) {
        route53.listHostedZones({}, function(err, data) {
            if (err) {
                console.log(err, err.stack);
                response.send(event, context, response.FAILED, err);
            }
            else {
                searchHostedZoneNames(event, context, route53, null, null, nextFunc);
            }
        });
    } else {
        response.send(event, context, response.FAILED, "Must specify HostedZoneId or HostedZoneName.");
    }
};

var searchHostedZoneNames = function(event, context, route53, marker, prevMatchResults, nextFunc) {
    var params = {};
    if (marker) {
        params = {'Marker': marker};
    }
    var fqdnToFind = event.ResourceProperties.HostedZoneName.endsWith('.')?event.ResourceProperties.HostedZoneName:(event.ResourceProperties.HostedZoneName + '.')

    route53.listHostedZones(params, function(err, data) {
        if (err) {
            console.log(err, err.stack);
            response.send(event, context, response.FAILED, err);
        }
        else {
            var matchResults = data.HostedZones.map(function(obj){
                var fqdnToCheck = obj.Name.endsWith('.')?obj.Name:(obj.Name + '.');

                if (fqdnToCheck == fqdnToFind) {
                    return obj.Id;
                } else {
                    return null;
                }
            }).filter(function(obj) {return obj != undefined});
            var aggregateResults = prevMatchResults?prevMatchResults.concat(matchResults):matchResults

            if (data.IsTruncated == true) {
                searchHostedZoneNames(event, context, route53, data.NextMarker, aggregateResults, nextFunc);
            } else {
                if (aggregateResults.length == 0) {
                    response.send(event, context, response.FAILED, "No zone found for HostedZoneName: " + event.ResourceProperties.HostedZoneName);
                } else if (aggregateResults.length > 1) {
                    response.send(event, context, response.FAILED, "Multiple zones found for HostedZoneName: " + event.ResourceProperties.HostedZoneName + " -- specify HostedZoneId.");
                } else {
                    nextFunc(event, context, route53, aggregateResults[0]);
                }
            }
        }
    });
};

var physicalId = function(properties) {
    return 'route53-record-set-' + properties.Name;
};

var sendSignal = function(handle, context, data){
    var body = JSON.stringify(data);
    var https = require("https");
    var url = require("url");
    console.log("signal body:\n", body);

    var parsedUrl = url.parse(handle);
    var options = {
        hostname: parsedUrl.hostname,
        port: 443,
        path: parsedUrl.path,
        method: "PUT",
        headers: {
            "content-type": "",
            "content-length": body.length
        }
    };

    var request = https.request(options, function(response) {
        console.log("Status code: " + response.statusCode);
        console.log("Status message: " + response.statusMessage);
        context.done();
    });

    request.on("error", function(error) {
        console.log("sendSignal(..) failed executing https.request(..): " + error);
        context.done();
    });

    request.write(body);
    request.end();
};

if (!String.prototype.endsWith) {
    String.prototype.endsWith = function(searchString, position) {
        var subjectString = this.toString();
        if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
            position = subjectString.length;
        }
        position -= searchString.length;
        var lastIndex = subjectString.indexOf(searchString, position);
        return lastIndex !== -1 && lastIndex === position;
    };
}

/* The below section is adapted from the cfn-response module, as published at:

 http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html

 */

/* Copyright 2015 Amazon Web Services, Inc. or its affiliates. All Rights Reserved.
 This file is licensed to you under the AWS Customer Agreement (the "License").
 You may not use this file except in compliance with the License.
 A copy of the License is located at http://aws.amazon.com/agreement/.
 This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied.
 See the License for the specific language governing permissions and limitations under the License. */
var response = {};

response.SUCCESS = "SUCCESS";
response.FAILED = "FAILED";

response.send = function(event, context, responseStatus, responseReason, responseData, physicalResourceId, continueFuncton) {
    reason = "CloudWatch Log Stream: " + context.logGroupName + " -- " + context.logStreamName
    if (responseReason) {
        reason = responseReason + " - " + reason
    }

    if (!responseData) {
        responseData = {}
    }

    var responseBody = JSON.stringify({
        Status: responseStatus,
        Reason: reason,
        PhysicalResourceId: physicalResourceId || context.logStreamName,
        StackId: event.StackId,
        RequestId: event.RequestId,
        LogicalResourceId: event.LogicalResourceId,
        Data: responseData
    });

    console.log("Response body:\n", responseBody);

    var https = require("https");
    var url = require("url");

    var parsedUrl = url.parse(event.ResponseURL);
    var options = {
        hostname: parsedUrl.hostname,
        port: 443,
        path: parsedUrl.path,
        method: "PUT",
        headers: {
            "content-type": "",
            "content-length": responseBody.length
        }
    };

    var request = https.request(options, function(response) {
        console.log("Status code: " + response.statusCode);
        console.log("Status message: " + response.statusMessage);
        if(!continueFuncton){
            context.done();
        }
    });

    request.on("error", function(error) {
        console.log("send(..) failed executing https.request(..): " + error);
        context.done();
    });

    request.write(responseBody);
    request.end();
};
<|endoftext|>


<commit_before>var Dispatcher = require.requireActual('../src/Dispatcher');

var Store = jest.genMockFromModule('../src/Store');
var SubscriptionHandler = jest.genMockFromModule('../src/SubscriptionHandler');

// Use manul mock
Store.createStore = function(initialState) {
	var state = Object.assign({}, initialState);
	var store = new Store(state, [], null);

	store.getState = jest.genMockFunction().mockImplementation(() => state);
	store.dispatch = jest.genMockFunction().mockReturnValue(Promise.resolve(store));
	store.startDispatchAt = jest.genMockFunction().mockImplementation((_, startingPoint) => {
		state = startingPoint.state;
		return Promise.resolve(store);
	});
	store.useIsoDispatcher = jest.genMockFunction().mockReturnValue(store);

	return store;
};

SubscriptionHandler.createSubscriptionHandler = function() {
	var subscriptionHandler = new SubscriptionHandler([]);

	subscriptionHandler.subscribe = jest.genMockFunction().mockReturnValue(subscriptionHandler);
	subscriptionHandler.unsubscribe = jest.genMockFunction().mockReturnValue(subscriptionHandler);
	subscriptionHandler.publish = jest.genMockFunction();

	return subscriptionHandler;
};

Dispatcher.createDispatcher = function(stores, updateSubscriptionHandler) {
	return new Dispatcher(
		stores,
		SubscriptionHandler.createSubscriptionHandler(updateSubscriptionHandler)
	);
};

function getStores(initialStates) {
	return {
		a: Store.createStore(initialStates? initialStates.a: {}),
		b: Store.createStore(initialStates? initialStates.b: {}),
		c: Store.createStore(initialStates? initialStates.c: {}),
		d: Store.createStore(initialStates? initialStates.d: {}),
		e: Store.createStore(initialStates? initialStates.e: {})
	};
}

describe('Dispatcher', () => {
	it('has the given stores', () => {
		var stores = getStores();
		var dispatcher = Dispatcher.createDispatcher(stores);

		// Test the given stores are added (and no others)
		expect(dispatcher._stores).toEqual(stores);	//TODO, fix to use public dispatcher api

		//Test trying to use invalid stores
		var invalidStores = [
			null,
			1,
			"invalid"
		];

		invalidStores.map((invalidStore) => {
			expect(() => {
				Dispatcher.createDispatcher(invalidStore);
			}).toThrow();

			expect(() => {
				Dispatcher.createDispatcher({ aName: invalidStore });
			}).toThrow();
		});
		expect(() => {
			Dispatcher.createDispatcher({ aName: {} });
		}).toThrow();
		expect(() => {
			Dispatcher.createDispatcher({ aName: [] });
		}).toThrow();
	});

	pit('dispatchs to all of its stores', () => {
		var dispatchedAction = { dispatchedAction: true };
		var dispatchFuncs = {};
		var stores = getStores();

		return Dispatcher.createDispatcher(stores).dispatch(dispatchedAction).then(() => {
			Object.keys(stores).forEach((storeName) => {
				// Test dispatcher was called
				var dispatchFunc = stores[storeName].dispatch;
				expect(dispatchFunc.mock.calls.length).toBe(1);

				// Test dispatcher was called with the correct object
				var dispatchFuncArg = dispatchFunc.mock.calls[0][0];
				expect(dispatchFuncArg).toBe(dispatchedAction);
			});
		});
	});

	it('can get the states of all of its stores', () => {
		var initialStates = {
			a: { stateFor: 'a' },
			b: { stateFor: 'b' },
			c: { stateFor: 'c' },
			d: { stateFor: 'd' },
			e: { stateFor: 'e' }
		};
		var stores = getStores(initialStates);
		var dispatcher = Dispatcher.createDispatcher(stores);

		// Test getting all states
		var allStates = dispatcher.getStateForAll();
		expect(allStates).toEqual(initialStates);

		// Test getting a single stores state
		Object.keys(initialStates).forEach((storeName) => {
			var currentState = dispatcher.getStateFor(storeName);

			expect(currentState).toEqual(initialStates[storeName]);
		});

		// Tests trying to get state from invalid stores
		var invalidStoreNames = [
			null,
			0,
			"not a store name",
			[],
			{}
		];
		invalidStoreNames.forEach((invalidStoreName) => {
			expect(() => {
				dispatcher.getStateFor(invalidStoreName);
			}).toThrow();
		});
	});

	it('allows function to subscribe to it', () => {
		var stores = getStores();
		var subscriptionHandler = SubscriptionHandler.createSubscriptionHandler();
		var dispatcher = Dispatcher.createDispatcher(stores, subscriptionHandler);

		var subscribers = [
			jest.genMockFunction(),
			jest.genMockFunction(),
			jest.genMockFunction(),
			jest.genMockFunction(),
			jest.genMockFunction()
		];
		var unsubscribeFuncs = subscribers.map((subscriber) => {
			return dispatcher.subscribeToAll(subscriber);
		});

		// Test subscribing to all stores
		subscriptionHandler = dispatcher._subscriptionHandler;
		subscriptionHandler.subscribe.mock.calls.forEach((subscribeArgs) => {
			expect(subscribers).toContain(subscribeArgs[0]);
		});
		expect(subscriptionHandler.subscribe.mock.calls.length).toBe(subscribers.length);

		// Unsubscribing from all stores
		unsubscribeFuncs.forEach((unsubscribeFunc, index) => {
			// Test unsubscribing
			unsubscribeFunc();
			subscriptionHandler.unsubscribe.mock.calls.forEach((unsubscribeArgs) => {
				expect(subscribers).toContain(unsubscribeArgs[0]);
			});

			// Test when subscriptionHandler.unsubscribe is called correct number of times
			expect(subscriptionHandler.unsubscribe.mock.calls.length).toBe(index + 1);

			// Test unsubscibing twice
			expect(() => {
				unsubscribeFunc();
			}).toThrow();
		});

		// Test subscribing to single stores
		subscriptionHandler = SubscriptionHandler.createSubscriptionHandler();
		dispatcher = Dispatcher.createDispatcher(stores, subscriptionHandler);
		unsubscribeFuncs = subscribers.map((subsciber) => {
			return dispatcher.subscribeTo('a', subsciber);
		});

		subscriptionHandler = dispatcher._subscriptionHandler;
		/*/ERROR, won't work because of groups
		subscriptionHandler.subscribe.mock.calls.forEach((subscibeArgs) => {
			expect(subscribers).toContain(subscibeArgs[0]);
		});//*/
		expect(subscriptionHandler.subscribe.mock.calls.length).toBe(subscribers.length);

		// Unsubscribing from single stores
		unsubscribeFuncs.forEach((unsubscribeFunc, index) => {
			// Test unsubscribing
			unsubscribeFunc();
			/*/ERROR, won't work because of groups
			subscriptionHandler.unsubscribe.mock.calls.forEach((unsubscribeArgs) => {
				expect(subscribers).toContain(unsubscribeArgs[0]);
			});//*/

			// Test when subscriptionHandler.unsubscribe is called correct number of times
			expect(subscriptionHandler.unsubscribe.mock.calls.length).toBe(index + 1);

			// Test unsubscibing twice
			expect(() => {
				unsubscribeFunc();
			}).toThrow();
		});

		// Test subscribing to invalid store name
		var invalidStoreNames = [
			null,
			1,
			true,
			"invalid store name",
			{},
			[]
		];
		invalidStoreNames.forEach((invalidStoreName) => {
			expect(() => {
				dispatcher.subscribeTo(invalidStoreName, jest.genMockFunction());
				console.log({ invalidStoreName });
			}).toThrow();
		});
	});

	pit('calls all of its subscribers with the stores state', () => {
		var initialStates = {
			a: { stateFor: 'a' },
			b: { stateFor: 'b' },
			c: { stateFor: 'c' },
			d: { stateFor: 'd' },
			e: { stateFor: 'e' }
		};
		var stores = getStores(initialStates);
		var subscriptionHandler = SubscriptionHandler.createSubscriptionHandler();
		var dispatcher = Dispatcher.createDispatcher(stores, subscriptionHandler);

		return dispatcher.dispatch({}).then(() => {
			subscriptionHandler = dispatcher._subscriptionHandler;

			// Test published value is the states of the stores
			expect(subscriptionHandler.publish.mock.calls[0][0]).toEqual(initialStates);
		});
	});
});

describe('ClientDispatcher', () => {
	pit.only('calls iso function when onServer is called', () => {
		var initialStates = {
			a: { stateFor: 'a' },
			b: { stateFor: 'b' },
			c: { stateFor: 'c' },
			d: { stateFor: 'd' },
			e: { stateFor: 'e' }
		};
		var statesFromServer = {
			a: { stateFromServerFor: 'a' },
			c: { stateFromServerFor: 'c' },
			e: { stateFromServerFor: 'e' }
		};
		var storesToPause = Object.keys(statesFromServer);
		var pauseAtStartingPoints = {};
		storesToPause.forEach((storeName) => {
			pauseAtStartingPoints[storeName] = { index: 0, state: initialStates[storeName] };
		});
		var dispatchedAction = { type: 'test action' };
		var finishOnServer = jest.genMockFunction().mockReturnValue(
			Promise.resolve(statesFromServer)
		);
		var stores = getStores(initialStates);

		// Update mock for stores that 'call' onServer
		storesToPause.forEach((storeToPause) => {
			var store = stores[storeToPause];

			store.dispatch = jest.genMockFunction().mockImplementation((action) => {
				// Test 'useIsoDispatcher' was called by ClientDispatcher
				expect(store.useIsoDispatcher.mock.calls.length).toBe(1);

				// Test correct action is sent
				expect(action).toEqual(dispatchedAction);

				// Call function passed to 'useIsoDispatcher'
				var useIsoDispatcherFunc = store.useIsoDispatcher.mock.calls[0][0];
				useIsoDispatcherFunc(action, pauseAtStartingPoints[storeToPause]);

				return Promise.resolve(store);
			});

			stores[storeToPause] = store;
		});

		var dispatcher = Dispatcher.createClientDispatcher(finishOnServer, stores);

		// Perform dispatch
		return dispatcher.dispatch(dispatchedAction).then((newStates) => {
			for(var storeName in newStates) {
				var newState = newStates[storeName];

				// Test finish on server was called
				expect(finishOnServer.mock.calls.length).toBe(1);
				finishOnServer.mock.calls.forEach(([action, pausePoints]) => {
					// Test correct action is sent
					expect(action).toEqual(dispatchedAction);

					// Test no extra stores where paused
					var pausePointStoreNames = Object.keys(pausePoints);
					expect(pausePointStoreNames.length).toBe(storesToPause.length);

					pausePointStoreNames.forEach((pausePointStoreName) => {
						var pausePoint = pausePoints[pausePointStoreName];

						// Test correct pause point index and state
						expect(pausePoint.index).toBe(0);
						expect(pausePoint.state).toEqual(initialStates[pausePointStoreName]);

						// Test correct stores where paused
						expect(storesToPause).toContain(pausePointStoreName);
					});
				});
			}
		});
	});
});

describe('ServerDispatcher', () => {
	it('can start stores in the middle', () => {
		//TODO, nyi: start dispatch in middle
		//TODO, nyi: has invalid start point when starting in middle
	});
});
<commit_msg>Fill in Server Dispatcher Tests<commit_after>var Dispatcher = require.requireActual('../src/Dispatcher');

var Store = jest.genMockFromModule('../src/Store');
var SubscriptionHandler = jest.genMockFromModule('../src/SubscriptionHandler');

// Use manul mock
Store.createStore = function(initialState) {
	var state = Object.assign({}, initialState);
	var store = new Store(state, [], null);

	store.getState = jest.genMockFunction().mockImplementation(() => state);
	store.dispatch = jest.genMockFunction().mockReturnValue(Promise.resolve(store));
	store.startDispatchAt = jest.genMockFunction().mockImplementation((_, startingPoint) => {
		state = startingPoint.state;
		return Promise.resolve(store);
	});
	store.useIsoDispatcher = jest.genMockFunction().mockReturnValue(store);

	return store;
};

SubscriptionHandler.createSubscriptionHandler = function() {
	var subscriptionHandler = new SubscriptionHandler([]);

	subscriptionHandler.subscribe = jest.genMockFunction().mockReturnValue(subscriptionHandler);
	subscriptionHandler.unsubscribe = jest.genMockFunction().mockReturnValue(subscriptionHandler);
	subscriptionHandler.publish = jest.genMockFunction();

	return subscriptionHandler;
};

Dispatcher.createDispatcher = function(stores, updateSubscriptionHandler) {
	return new Dispatcher(
		stores,
		SubscriptionHandler.createSubscriptionHandler(updateSubscriptionHandler)
	);
};

function getStores(initialStates) {
	return {
		a: Store.createStore(initialStates? initialStates.a: {}),
		b: Store.createStore(initialStates? initialStates.b: {}),
		c: Store.createStore(initialStates? initialStates.c: {}),
		d: Store.createStore(initialStates? initialStates.d: {}),
		e: Store.createStore(initialStates? initialStates.e: {})
	};
}

describe('Dispatcher', () => {
	it('has the given stores', () => {
		var stores = getStores();
		var dispatcher = Dispatcher.createDispatcher(stores);

		// Test the given stores are added (and no others)
		expect(dispatcher._stores).toEqual(stores);	//TODO, fix to use public dispatcher api

		//Test trying to use invalid stores
		var invalidStores = [
			null,
			1,
			"invalid"
		];

		invalidStores.map((invalidStore) => {
			expect(() => {
				Dispatcher.createDispatcher(invalidStore);
			}).toThrow();

			expect(() => {
				Dispatcher.createDispatcher({ aName: invalidStore });
			}).toThrow();
		});
		expect(() => {
			Dispatcher.createDispatcher({ aName: {} });
		}).toThrow();
		expect(() => {
			Dispatcher.createDispatcher({ aName: [] });
		}).toThrow();
	});

	pit('dispatchs to all of its stores', () => {
		var dispatchedAction = { dispatchedAction: true };
		var dispatchFuncs = {};
		var stores = getStores();

		return Dispatcher.createDispatcher(stores).dispatch(dispatchedAction).then(() => {
			Object.keys(stores).forEach((storeName) => {
				// Test dispatcher was called
				var dispatchFunc = stores[storeName].dispatch;
				expect(dispatchFunc.mock.calls.length).toBe(1);

				// Test dispatcher was called with the correct object
				var dispatchFuncArg = dispatchFunc.mock.calls[0][0];
				expect(dispatchFuncArg).toBe(dispatchedAction);
			});
		});
	});

	it('can get the states of all of its stores', () => {
		var initialStates = {
			a: { stateFor: 'a' },
			b: { stateFor: 'b' },
			c: { stateFor: 'c' },
			d: { stateFor: 'd' },
			e: { stateFor: 'e' }
		};
		var stores = getStores(initialStates);
		var dispatcher = Dispatcher.createDispatcher(stores);

		// Test getting all states
		var allStates = dispatcher.getStateForAll();
		expect(allStates).toEqual(initialStates);

		// Test getting a single stores state
		Object.keys(initialStates).forEach((storeName) => {
			var currentState = dispatcher.getStateFor(storeName);

			expect(currentState).toEqual(initialStates[storeName]);
		});

		// Tests trying to get state from invalid stores
		var invalidStoreNames = [
			null,
			0,
			"not a store name",
			[],
			{}
		];
		invalidStoreNames.forEach((invalidStoreName) => {
			expect(() => {
				dispatcher.getStateFor(invalidStoreName);
			}).toThrow();
		});
	});

	it('allows function to subscribe to it', () => {
		var stores = getStores();
		var subscriptionHandler = SubscriptionHandler.createSubscriptionHandler();
		var dispatcher = Dispatcher.createDispatcher(stores, subscriptionHandler);

		var subscribers = [
			jest.genMockFunction(),
			jest.genMockFunction(),
			jest.genMockFunction(),
			jest.genMockFunction(),
			jest.genMockFunction()
		];
		var unsubscribeFuncs = subscribers.map((subscriber) => {
			return dispatcher.subscribeToAll(subscriber);
		});

		// Test subscribing to all stores
		subscriptionHandler = dispatcher._subscriptionHandler;
		subscriptionHandler.subscribe.mock.calls.forEach((subscribeArgs) => {
			expect(subscribers).toContain(subscribeArgs[0]);
		});
		expect(subscriptionHandler.subscribe.mock.calls.length).toBe(subscribers.length);

		// Unsubscribing from all stores
		unsubscribeFuncs.forEach((unsubscribeFunc, index) => {
			// Test unsubscribing
			unsubscribeFunc();
			subscriptionHandler.unsubscribe.mock.calls.forEach((unsubscribeArgs) => {
				expect(subscribers).toContain(unsubscribeArgs[0]);
			});

			// Test when subscriptionHandler.unsubscribe is called correct number of times
			expect(subscriptionHandler.unsubscribe.mock.calls.length).toBe(index + 1);

			// Test unsubscibing twice
			expect(() => {
				unsubscribeFunc();
			}).toThrow();
		});

		// Test subscribing to single stores
		subscriptionHandler = SubscriptionHandler.createSubscriptionHandler();
		dispatcher = Dispatcher.createDispatcher(stores, subscriptionHandler);
		unsubscribeFuncs = subscribers.map((subsciber) => {
			return dispatcher.subscribeTo('a', subsciber);
		});

		subscriptionHandler = dispatcher._subscriptionHandler;
		/*/ERROR, won't work because of groups
		subscriptionHandler.subscribe.mock.calls.forEach((subscibeArgs) => {
			expect(subscribers).toContain(subscibeArgs[0]);
		});//*/
		expect(subscriptionHandler.subscribe.mock.calls.length).toBe(subscribers.length);

		// Unsubscribing from single stores
		unsubscribeFuncs.forEach((unsubscribeFunc, index) => {
			// Test unsubscribing
			unsubscribeFunc();
			/*/ERROR, won't work because of groups
			subscriptionHandler.unsubscribe.mock.calls.forEach((unsubscribeArgs) => {
				expect(subscribers).toContain(unsubscribeArgs[0]);
			});//*/

			// Test when subscriptionHandler.unsubscribe is called correct number of times
			expect(subscriptionHandler.unsubscribe.mock.calls.length).toBe(index + 1);

			// Test unsubscibing twice
			expect(() => {
				unsubscribeFunc();
			}).toThrow();
		});

		// Test subscribing to invalid store name
		var invalidStoreNames = [
			null,
			1,
			true,
			"invalid store name",
			{},
			[]
		];
		invalidStoreNames.forEach((invalidStoreName) => {
			expect(() => {
				dispatcher.subscribeTo(invalidStoreName, jest.genMockFunction());
				console.log({ invalidStoreName });
			}).toThrow();
		});
	});

	pit('calls all of its subscribers with the stores state', () => {
		var initialStates = {
			a: { stateFor: 'a' },
			b: { stateFor: 'b' },
			c: { stateFor: 'c' },
			d: { stateFor: 'd' },
			e: { stateFor: 'e' }
		};
		var stores = getStores(initialStates);
		var subscriptionHandler = SubscriptionHandler.createSubscriptionHandler();
		var dispatcher = Dispatcher.createDispatcher(stores, subscriptionHandler);

		return dispatcher.dispatch({}).then(() => {
			subscriptionHandler = dispatcher._subscriptionHandler;

			// Test published value is the states of the stores
			expect(subscriptionHandler.publish.mock.calls[0][0]).toEqual(initialStates);
		});
	});
});

describe('ClientDispatcher', () => {
	pit.only('calls iso function when onServer is called', () => {
		var initialStates = {
			a: { stateFor: 'a' },
			b: { stateFor: 'b' },
			c: { stateFor: 'c' },
			d: { stateFor: 'd' },
			e: { stateFor: 'e' }
		};
		var statesFromServer = {
			a: { stateFromServerFor: 'a' },
			c: { stateFromServerFor: 'c' },
			e: { stateFromServerFor: 'e' }
		};
		var storesToPause = Object.keys(statesFromServer);
		var pauseAtStartingPoints = {};
		storesToPause.forEach((storeName) => {
			pauseAtStartingPoints[storeName] = { index: 0, state: initialStates[storeName] };
		});
		var dispatchedAction = { type: 'test action' };
		var finishOnServer = jest.genMockFunction().mockReturnValue(
			Promise.resolve(statesFromServer)
		);
		var stores = getStores(initialStates);

		// Update mock for stores that 'call' onServer
		storesToPause.forEach((storeToPause) => {
			var store = stores[storeToPause];

			store.dispatch = jest.genMockFunction().mockImplementation((action) => {
				// Test 'useIsoDispatcher' was called by ClientDispatcher
				expect(store.useIsoDispatcher.mock.calls.length).toBe(1);

				// Test correct action is sent
				expect(action).toEqual(dispatchedAction);

				// Call function passed to 'useIsoDispatcher'
				var useIsoDispatcherFunc = store.useIsoDispatcher.mock.calls[0][0];
				useIsoDispatcherFunc(action, pauseAtStartingPoints[storeToPause]);

				return Promise.resolve(store);
			});

			stores[storeToPause] = store;
		});

		var dispatcher = Dispatcher.createClientDispatcher(finishOnServer, stores);

		// Perform dispatch
		return dispatcher.dispatch(dispatchedAction).then((newStates) => {
			for(var storeName in newStates) {
				var newState = newStates[storeName];

				// Test finish on server was called
				expect(finishOnServer.mock.calls.length).toBe(1);
				finishOnServer.mock.calls.forEach(([action, pausePoints]) => {
					// Test correct action is sent
					expect(action).toEqual(dispatchedAction);

					// Test no extra stores where paused
					var pausePointStoreNames = Object.keys(pausePoints);
					expect(pausePointStoreNames.length).toBe(storesToPause.length);

					pausePointStoreNames.forEach((pausePointStoreName) => {
						var pausePoint = pausePoints[pausePointStoreName];

						// Test correct pause point index and state
						expect(pausePoint.index).toBe(0);
						expect(pausePoint.state).toEqual(initialStates[pausePointStoreName]);

						// Test correct stores where paused
						expect(storesToPause).toContain(pausePointStoreName);
					});
				});
			}
		});
	});
});

describe('ServerDispatcher', () => {
	pit('can start stores in the middle', () => {
		var promises = [];

		var initialStates = {
			a: { stateFor: 'a' },
			b: { stateFor: 'b' },
			c: { stateFor: 'c' },
			d: { stateFor: 'd' },
			e: { stateFor: 'e' }
		};
		var action = { type: 'test action' };
		var startingPoints = {
			a: { index: 0, state: { newStateFor: 'a' } },
			c: { index: 0, state: { newStateFor: 'c' } },
			e: { index: 0, state: { newStateFor: 'e' } }
		};
		var passedArg = { passed: 'arg' };
		var stores = getStores(initialStates);
		var dispatcher = Dispatcher.createServerDispatcher(stores);

		promises.push(
			dispatcher.startDispatchAt(action, startingPoints, passedArg).then((newStates) => {
				for(var storeName in stores) {
					var store = stores[storeName];
					var startDispatchAtCalls = store.startDispatchAt.mock.calls;
					var startingPoint = startingPoints[storeName];

					// Test state is correct
					if(startingPoint) {
						expect(newStates[storeName]).toEqual(startingPoint.state);
					}
					else {
						expect(newStates[storeName]).toBeUndefined();
					}

					// Test correct the store.startDispatchAt was called
					if(startingPoint) {
						expect(startDispatchAtCalls.length).toBe(1);
						expect(startDispatchAtCalls[0]).toEqual([action, startingPoint, passedArg]);
					}
					else {
						expect(startDispatchAtCalls.length).toBe(0);
					}
				}
			})
		);

		// Test invalid starting points
		var invalidStartingPoints = [
			null,
			1,
			true,
			{ a: null },
			{ a: 1 },
			{ a: true }
		];
		invalidStartingPoints.forEach((invalidStartingPoint) => {
			dispatcher = Dispatcher.createServerDispatcher(stores);
			promises.push(
				dispatcher.startDispatchAt(action, invalidStartingPoint, passedArg)
					.then(() => {
						throw new Error(
							'dispatcher.startDispatchAt return state, instead of an error'
						);
					})
					.catch((err) => {
						expect(err.message).toBe(
							'starting point must be an object of starting points'
						);
					})
			);
		});

		return Promise.all(promises);
	});
});
<|endoftext|>


<commit_before>/*
    ***** BEGIN LICENSE BLOCK *****

    Copyright © 2011 Center for History and New Media
                     George Mason University, Fairfax, Virginia, USA
                     http://zotero.org

    This file is part of Zotero.

    Zotero is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Zotero is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Zotero.  If not, see <http://www.gnu.org/licenses/>.

    ***** END LICENSE BLOCK *****
*/

/**
 * Emulates very small parts of cachedTypes.js and itemFields.js APIs for use with connector
 */

/**
 * @namespace
 */
Zotero.Connector_Types = new function() {
	/**
	 * Initializes types
	 * @param {Object} typeSchema typeSchema generated by Zotero.Connector.GetData#_generateTypeSchema
	 */
	this.init = function() {
		const schemaTypes = ["itemTypes", "creatorTypes", "fields"];

		// attach IDs and make referenceable by either ID or name
		for(var i=0; i<schemaTypes.length; i++) {
			var schemaType = schemaTypes[i];
			this[schemaType] = Zotero.Utilities.deepCopy(Zotero.Connector_Types.schema[schemaType]);
			for(var id in Zotero.Connector_Types.schema[schemaType]) {
				var entry = this[schemaType][id];
				entry.unshift(parseInt(id, 10));
				this[schemaType][entry[1]/* name */] = entry;
			}
		}

		var itemTypes = Zotero.Connector_Types["itemTypes"];
		var creatorTypes = Zotero.Connector_Types["creatorTypes"];
		var fields = Zotero.Connector_Types["fields"];

		Zotero.CachedTypes = function() {
			var thisType = Zotero.Connector_Types[this.schemaType];

			this.getID = function(idOrName) {
				var type = thisType[idOrName];
				return (type ? type[0]/* id */ : false);
			};

			this.getName = function(idOrName) {
				var type = thisType[idOrName];
				return (type ? type[1]/* name */ : false);
			};

			this.getLocalizedString = function(idOrName) {
				var type = thisType[idOrName];
				return (type ? type[2]/* localizedString */ : false);
			};
		}

		Zotero.ItemTypes = new function() {
			this.schemaType = "itemTypes";
			Zotero.CachedTypes.call(this);

			this.getImageSrc = function(idOrName) {
				var itemType = Zotero.Connector_Types["itemTypes"][idOrName];
				var icon = itemType ? itemType[6]/* icon */ : "treeitem-"+idOrName+".png";

				if(Zotero.isBookmarklet) {
					return ZOTERO_CONFIG.BOOKMARKLET_URL+"images/"+icon;
				} else if(Zotero.isFx) {
					return "chrome://zotero/skin/"+icon;
				} else if(Zotero.isChrome) {
					return chrome.extension.getURL("images/"+icon);
				} else if(Zotero.isSafari) {
					return safari.extension.baseURI+"images/"+icon;
				}
			};
		}

		Zotero.CreatorTypes = new function() {
			this.schemaType = "creatorTypes";
			Zotero.CachedTypes.call(this);

			this.getTypesForItemType = function(idOrName) {
				var itemType = itemTypes[idOrName];
				if(!itemType) return false;

				var itemCreatorTypes = itemType[3]/* creatorTypes */,
					n = itemCreatorTypes.length,
					outputTypes = new Array(n);

				for(var i=0; i<n; i++) {
					var creatorType = creatorTypes[itemCreatorTypes[i]];
					outputTypes[i] = {"id":creatorType[0]/* id */,
						"name":creatorType[1]/* name */};
				}
				return outputTypes;
			};

			this.getPrimaryIDForType = function(idOrName) {
				var itemType = itemTypes[idOrName];
				if(!itemType) return false;
				return itemTypes[3]/* creatorTypes */[0];
			};
		}

		Zotero.ItemFields = new function() {
			this.schemaType = "fields";
			Zotero.CachedTypes.call(this);

			this.isValidForType = function(fieldIdOrName, typeIdOrName) {
				var field = fields[fieldIdOrName], itemType = itemTypes[typeIdOrName];

				// mimics itemFields.js
				if(!field || !itemType) return false;

				       /* fields */        /* id */
				return itemType[4].indexOf(field[0]) !== -1;
			};

			this.getFieldIDFromTypeAndBase = function(typeIdOrName, fieldIdOrName) {
				var baseField = fields[fieldIdOrName], itemType = itemTypes[typeIdOrName];

				if(!baseField || !itemType) return false;

				// get as ID
				baseField = baseField[0]/* id */;

				// loop through base fields for item type
				var baseFields = itemType[5];
				for(var i in baseFields) {
					if(baseFields[i] === baseField) {
						return i;
					}
				}

				return false;
			};

			this.getBaseIDFromTypeAndField = function(typeIdOrName, fieldIdOrName) {
				var field = fields[fieldIdOrName], itemType = itemTypes[typeIdOrName];
				if(!field || !itemType) {
					throw new Error("Invalid field or type ID");
				}

				var baseField = itemType[5]/* baseFields */[field[0]/* id */];
				return baseField ? baseField : false;
			};

			this.getItemTypeFields = function(typeIdOrName) {
				return itemTypes[typeIdOrName][4]/* fields */.slice();
			};
		}
	};

	/**
	 * Passes schema to a callback
	 * @param {Function} callback
	 */
	this.getSchema = function(callback) {
		callback(Zotero.Connector_Types.schema);
	};
}<commit_msg>Fixes #876, Uncaught error in getTypesForItemType with valid input<commit_after>/*
    ***** BEGIN LICENSE BLOCK *****

    Copyright © 2011 Center for History and New Media
                     George Mason University, Fairfax, Virginia, USA
                     http://zotero.org

    This file is part of Zotero.

    Zotero is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Zotero is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Zotero.  If not, see <http://www.gnu.org/licenses/>.

    ***** END LICENSE BLOCK *****
*/

/**
 * Emulates very small parts of cachedTypes.js and itemFields.js APIs for use with connector
 */

/**
 * @namespace
 */
Zotero.Connector_Types = new function() {
	/**
	 * Initializes types
	 * @param {Object} typeSchema typeSchema generated by Zotero.Connector.GetData#_generateTypeSchema
	 */
	this.init = function() {
		const schemaTypes = ["itemTypes", "creatorTypes", "fields"];

		// attach IDs and make referenceable by either ID or name
		for(var i=0; i<schemaTypes.length; i++) {
			var schemaType = schemaTypes[i];
			this[schemaType] = Zotero.Utilities.deepCopy(Zotero.Connector_Types.schema[schemaType]);
			for(var id in Zotero.Connector_Types.schema[schemaType]) {
				var entry = this[schemaType][id];
				entry.unshift(parseInt(id, 10));
				this[schemaType][entry[1]/* name */] = entry;
			}
		}

		var itemTypes = Zotero.Connector_Types["itemTypes"];
		var creatorTypes = Zotero.Connector_Types["creatorTypes"];
		var fields = Zotero.Connector_Types["fields"];

		Zotero.CachedTypes = function() {
			var thisType = Zotero.Connector_Types[this.schemaType];

			this.getID = function(idOrName) {
				var type = thisType[idOrName];
				return (type ? type[0]/* id */ : false);
			};

			this.getName = function(idOrName) {
				var type = thisType[idOrName];
				return (type ? type[1]/* name */ : false);
			};

			this.getLocalizedString = function(idOrName) {
				var type = thisType[idOrName];
				return (type ? type[2]/* localizedString */ : false);
			};
		}

		Zotero.ItemTypes = new function() {
			this.schemaType = "itemTypes";
			Zotero.CachedTypes.call(this);

			this.getImageSrc = function(idOrName) {
				var itemType = Zotero.Connector_Types["itemTypes"][idOrName];
				var icon = itemType ? itemType[6]/* icon */ : "treeitem-"+idOrName+".png";

				if(Zotero.isBookmarklet) {
					return ZOTERO_CONFIG.BOOKMARKLET_URL+"images/"+icon;
				} else if(Zotero.isFx) {
					return "chrome://zotero/skin/"+icon;
				} else if(Zotero.isChrome) {
					return chrome.extension.getURL("images/"+icon);
				} else if(Zotero.isSafari) {
					return safari.extension.baseURI+"images/"+icon;
				}
			};
		}

		Zotero.CreatorTypes = new function() {
			this.schemaType = "creatorTypes";
			Zotero.CachedTypes.call(this);

			this.getTypesForItemType = function(idOrName) {
				var itemType = itemTypes[idOrName];
				if(!itemType) return false;

				var itemCreatorTypes = itemType[3]; // creatorTypes
				if (!itemCreatorTypes
						// TEMP: 'note' and 'attachment' have an array containing false
						|| (itemCreatorTypes.length == 1 && !itemCreatorTypes[0])) {
					return [];
				}
				var n = itemCreatorTypes.length;
				var outputTypes = new Array(n);

				for(var i=0; i<n; i++) {
					var creatorType = creatorTypes[itemCreatorTypes[i]];
					outputTypes[i] = {"id":creatorType[0]/* id */,
						"name":creatorType[1]/* name */};
				}
				return outputTypes;
			};

			this.getPrimaryIDForType = function(idOrName) {
				var itemType = itemTypes[idOrName];
				if(!itemType) return false;
				return itemTypes[3]/* creatorTypes */[0];
			};
		}

		Zotero.ItemFields = new function() {
			this.schemaType = "fields";
			Zotero.CachedTypes.call(this);

			this.isValidForType = function(fieldIdOrName, typeIdOrName) {
				var field = fields[fieldIdOrName], itemType = itemTypes[typeIdOrName];

				// mimics itemFields.js
				if(!field || !itemType) return false;

				       /* fields */        /* id */
				return itemType[4].indexOf(field[0]) !== -1;
			};

			this.getFieldIDFromTypeAndBase = function(typeIdOrName, fieldIdOrName) {
				var baseField = fields[fieldIdOrName], itemType = itemTypes[typeIdOrName];

				if(!baseField || !itemType) return false;

				// get as ID
				baseField = baseField[0]/* id */;

				// loop through base fields for item type
				var baseFields = itemType[5];
				for(var i in baseFields) {
					if(baseFields[i] === baseField) {
						return i;
					}
				}

				return false;
			};

			this.getBaseIDFromTypeAndField = function(typeIdOrName, fieldIdOrName) {
				var field = fields[fieldIdOrName], itemType = itemTypes[typeIdOrName];
				if(!field || !itemType) {
					throw new Error("Invalid field or type ID");
				}

				var baseField = itemType[5]/* baseFields */[field[0]/* id */];
				return baseField ? baseField : false;
			};

			this.getItemTypeFields = function(typeIdOrName) {
				return itemTypes[typeIdOrName][4]/* fields */.slice();
			};
		}
	};

	/**
	 * Passes schema to a callback
	 * @param {Function} callback
	 */
	this.getSchema = function(callback) {
		callback(Zotero.Connector_Types.schema);
	};
}<|endoftext|>


<commit_before>/*---------------------------------------------------------
 * OpenERP web_graph
 *---------------------------------------------------------*/

openerp.web_graph = function (instance) {

var QWeb = instance.web.qweb,
     _lt = instance.web._lt;

instance.web.views.add('graph', 'instance.web_graph.GraphView');
instance.web_graph.GraphView = instance.web.View.extend({
    template: "GraphView",
    display_name: _lt('Graph'),
    view_type: "graph",

    init: function(parent, dataset, view_id, options) {
        this._super(parent);
        this.set_default_options(options);
        this.dataset = dataset;
        this.view_id = view_id;

        this.mode="bar";          // line, bar, area, pie, radar
        this.orientation=false;    // true: horizontal, false: vertical
        this.stacked=true;

        this.spreadsheet=false;   // Display data gris, allows copy to CSV
        this.forcehtml=false;
        this.legend_container;
        this.legend="top";        // top, inside, no

        this.domain = [];
        this.context = {};
        this.group_by = {};

        this.is_loaded = $.Deferred();
        this.graph = null;
    },
    destroy: function () {
        if (this.graph)
            this.graph.destroy();
        this._super();
    },

    on_loaded: function(fields_view_get) {
        // TODO: move  to load_view and document
        var width, height;
        var self = this;
        this.fields_view = fields_view_get;
        this.container = this.$element.find("#editor-render-body");

        this.mode = this.fields_view.arch.attrs.type || 'bar';
        this.orientation = this.fields_view.arch.attrs.orientation == 'horizontal';

        width = this.$element.parent().width();
        this.container.css("width", width);
        this.$element.css("width", width);
        this.container.css("height", Math.min(500, width*0.8));
        this.container = this.container[0];

        this.$element.find("#graph_bar,#graph_bar_stacked").click(
            {mode: 'bar', stacked: true, legend: 'top'}, $.proxy(this,"graph_render"))

        this.$element.find("#graph_bar_not_stacked").click(
            {mode: 'bar', stacked: false, legend: 'top'}, $.proxy(this,"graph_render"))

        this.$element.find("#graph_area,#graph_area_stacked").click(
            {mode: "area", stacked: true, legend: "top"}, $.proxy(this,"graph_render"));

        this.$element.find("#graph_area_not_stacked").click(
            {mode: "area", stacked: false, legend: "top"}, $.proxy(this,"graph_render"));

        this.$element.find("#graph_radar").click(
            {orientation: 0, mode: "radar", legend: "inside"}, $.proxy(this,"graph_render"));

        this.$element.find("#graph_pie").click(
            {mode: "pie", legend: "inside"}, $.proxy(this,"graph_render"));

        this.$element.find("#graph_legend_top").click(
            {legend: "top"}, $.proxy(self,"graph_render"));

        this.$element.find("#graph_legend_inside").click(
            {legend: "inside"}, $.proxy(self,"graph_render"));

        this.$element.find("#graph_legend_no").click(
            {legend: "no"}, $.proxy(self,"graph_render"));

        this.$element.find("#graph_line").click(
            {mode: "line"}, $.proxy(this,"graph_render"));

        this.$element.find("#graph_show_data").click(
            function() {
                self.spreadsheet = ! self.spreadsheet;
                self.graph_render();
            }
        );
        this.$element.find("#graph_switch").click(
            function() {
                if (this.mode != 'radar')
                    self.orientation = ! self.orientation;
                self.graph_render();
            }
        );

        this.$element.find("#graph_download").click(
            function() {
                if (Flotr.isIE && Flotr.isIE < 9) {
                    alert(
                        "Your browser doesn't allow you to get a bitmap image from the plot, " +
                        "you can only get a VML image that you can use in Microsoft Office."
                    );
                }
                if (self.legend=="top") self.legend="inside";
                self.forcehtml = true;
		return self.graph_get_data(
		    function (result) {
		    	var graph;
			graph = self.graph_render_all(result)
			graph.download.saveImage('png');
			self.forcehtml = false;
		    }
		);
            }
        );
        this._super();
    },

    get_format: function get_format(options) {
         var result = {
            show: this.legend!='no',
        }
        if (this.legend=="top") {
            result.noColumns = 4;
            result.container = this.$element.find("div.graph_header_legend")[0];
        } else if (this.legend=="inside") {
            result.position = 'nw';
            result.backgroundColor = '#D2E8FF';
        }
        return $.extend({
            legend: result,
            mouse: {
                track: true,
                relative: true
            },
            spreadsheet : {
                show: this.spreadsheet,
                initialTab: "data"
            },
            HtmlText : (options && options.xaxis && options.xaxis.labelsAngle)?false:!this.forcehtml,
        }, options)
    },

    graph_bar: function (container, data) {
        return Flotr.draw(container, data.data, this.get_format({
                bars : {
                    show : true,
                    stacked : this.stacked,
                    horizontal : this.orientation,
                    barWidth : 0.7,
                    lineWidth : 1
                },
                grid : {
                    verticalLines : this.orientation,
                    horizontalLines : !this.orientation,
                    outline : "sw",
                },
                yaxis : {
                    ticks: this.orientation?data.ticks:false
                },
                xaxis : {
                    labelsAngle: 45,
                    ticks: this.orientation?false:data.ticks
                }
            })
        )
    },

    graph_pie: function (container, data) {
        return Flotr.draw(container, data.data, this.get_format({
                pie : {
                    show: true
                },
                grid : {
                    verticalLines : false,
                    horizontalLines : false,
                    outline : "",
                },
                xaxis :  {showLabels: false},
                yaxis :  {showLabels: false},
            })
        )
    },

    graph_radar: function (container, data) {
        return Flotr.draw(container, data.data, this.get_format({
                radar : {
                    show : true,
                    stacked : this.stacked
                },
                grid : {
                    circular : true,
                    minorHorizontalLines : true
                },
                xaxis : {
                    ticks: data.ticks
                },
            })
        )
    },

    graph_line: function (container, data) {
        return Flotr.draw(container, data.data, this.get_format({
                lines : {
                    show : true,
                    stacked : this.stacked
                },
                grid : {
                    verticalLines : this.orientation,
                    horizontalLines : !this.orientation,
                    outline : "sw",
                },
                yaxis : {
                    ticks: this.orientation?data.ticks:false
                },
                xaxis : {
                    labelsAngle: 45,
                    ticks: this.orientation?false:data.ticks
                }
            })
        )
    },

    graph_get_data: function (callback) {
        var data = this.rpc(
            '/web_graph/graph/data_get',
            {
                model: this.dataset.model,
                domain: this.domain,
                context: this.context,
                group_by: this.group_by,
                view_id: this.view_id,
                mode: this.mode,
                orientation: this.orientation,
                stacked: this.stacked
            }, callback
        );
		return data
    },

    // Render the graph and update menu styles
    graph_render: function (options) {
        var i;
        var self = this;
        if (options)
            for (i in options.data)
                this[i] = options.data[i];

        return this.graph_get_data(
            function (result) {
                self.graph_render_all(result)
            }
        );
    },

    graph_render_all: function (data) {
        var graph;
	var i;
	if (this.mode=='area')
	    for (i=0; i<data.data.length; i++) {
		data.data[i].lines = {fill: true}
	    }
        if (this.graph)
            this.graph.destroy();

        // Render the graph
        this.$element.find(".graph_header_legend").children().remove()
        this.graph = {
            radar: $.proxy(this, "graph_radar"),
            pie: $.proxy(this, "graph_pie"),
            bar: $.proxy(this, "graph_bar"),
            area: $.proxy(this, "graph_line"),
            line: $.proxy(this, "graph_line")
        }[this.mode](this.container, data)

        // Update styles of menus

        this.$element.find("a[id^='graph_']").removeClass("active");
        this.$element.find("a[id='graph_"+this.mode+"']").addClass("active");
        this.$element.find("a[id='graph_"+this.mode+(this.stacked?"_stacked":"_not_stacked")+"']").addClass("active");

        if (this.legend=='inside')
            this.$element.find("a[id='graph_legend_inside']").addClass("active");
        else if (this.legend=='top')
            this.$element.find("a[id='graph_legend_top']").addClass("active");
        else
            this.$element.find("a[id='graph_legend_no']").addClass("active");

        if (this.spreadsheet)
            this.$element.find("a[id='graph_show_data']").addClass("active");
        return this.graph;
    },

    // render the graph using the domain, context and group_by
    // calls the 'graph_data_get' python controller to process all data
    // TODO: check is group_by should better be in the context
    do_search: function(domain, context, group_by) {
        this.domain = domain;
        this.context = context;
        this.group_by = group_by;

        this.graph_render();
        //});
    },

    do_show: function() {
        this.do_push_state({});
        return this._super();
    },

});
};
<commit_msg>[FIX] styllistic fixes|<commit_after>/*---------------------------------------------------------
 * OpenERP web_graph
 *---------------------------------------------------------*/

openerp.web_graph = function (instance) {

var _lt = instance.web._lt;

instance.web.views.add('graph', 'instance.web_graph.GraphView');
instance.web_graph.GraphView = instance.web.View.extend({
    template: "GraphView",
    display_name: _lt('Graph'),
    view_type: "graph",

    init: function(parent, dataset, view_id, options) {
        this._super(parent);
        this.set_default_options(options);
        this.dataset = dataset;
        this.view_id = view_id;

        this.mode = "bar";          // line, bar, area, pie, radar
        this.orientation = false;    // true: horizontal, false: vertical
        this.stacked = true;

        this.spreadsheet = false;   // Display data grid, allows copy to CSV
        this.forcehtml = false;
        this.legend = "top";        // top, inside, no

        this.domain = [];
        this.context = {};
        this.group_by = [];

        this.graph = null;
    },
    destroy: function () {
        if (this.graph) {
            this.graph.destroy();
        }
        this._super();
    },

    on_loaded: function(fields_view_get) {
        // TODO: move  to load_view and document
        var self = this;
        this.fields_view = fields_view_get;

        this.mode = this.fields_view.arch.attrs.type || 'bar';
        this.orientation = this.fields_view.arch.attrs.orientation == 'horizontal';

        var width = this.$element.parent().width();
        this.$element.css("width", width);
        this.container = this.$element.find("#editor-render-body").css({
            width: width,
            height: Math.min(500, width * 0.8)
        })[0];

        var graph_render = this.proxy('graph_render');
        this.$element.find("#graph_bar,#graph_bar_stacked").click(
            {mode: 'bar', stacked: true, legend: 'top'}, graph_render);

        this.$element.find("#graph_bar_not_stacked").click(
            {mode: 'bar', stacked: false, legend: 'top'}, graph_render);

        this.$element.find("#graph_area,#graph_area_stacked").click(
            {mode: "area", stacked: true, legend: "top"}, graph_render);

        this.$element.find("#graph_area_not_stacked").click(
            {mode: "area", stacked: false, legend: "top"}, graph_render);

        this.$element.find("#graph_radar").click(
            {orientation: 0, mode: "radar", legend: "inside"}, graph_render);

        this.$element.find("#graph_pie").click(
            {mode: "pie", legend: "inside"}, graph_render);

        this.$element.find("#graph_legend_top").click(
            {legend: "top"}, graph_render);

        this.$element.find("#graph_legend_inside").click(
            {legend: "inside"}, graph_render);

        this.$element.find("#graph_legend_no").click(
            {legend: "no"}, graph_render);

        this.$element.find("#graph_line").click(
            {mode: "line"}, graph_render);

        this.$element.find("#graph_show_data").click(function () {
            self.spreadsheet = ! self.spreadsheet;
            self.graph_render();
        });
        this.$element.find("#graph_switch").click(function () {
            if (self.mode != 'radar') {
                self.orientation = ! self.orientation;
            }
            self.graph_render();
        });

        this.$element.find("#graph_download").click(function () {
            if (self.legend == "top") { self.legend = "inside"; }
            self.forcehtml = true;

            self.graph_get_data().then(function (result) {
                self.graph_render_all(result).download.saveImage('png');
            }).always(function () {
                self.forcehtml = false;
            });
        });
        return this._super();
    },

    get_format: function (options) {
        options = options || {};
        var legend = {
            show: this.legend != 'no',
        };

        switch (this.legend) {
        case 'top':
            legend.noColumns = 4;
            legend.container = this.$element.find("div.graph_header_legend")[0];
            break;
        case 'inside':
            legend.position = 'nw';
            legend.backgroundColor = '#D2E8FF';
            break;
        }

        return _.extend({
            legend: legend,
            mouse: {
                track: true,
                relative: true
            },
            spreadsheet : {
                show: this.spreadsheet,
                initialTab: "data"
            },
            HtmlText : (options.xaxis && options.xaxis.labelsAngle) ? false : !this.forcehtml,
        }, options);
    },

    make_graph: function (mode, container, data) {
        if (mode === 'area') { mode = 'line'; }
        return Flotr.draw(
            container, data.data,
            this.get_format(this['options_' + mode](data)));
    },

    options_bar: function (data) {
        return {
            bars : {
                show : true,
                stacked : this.stacked,
                horizontal : this.orientation,
                barWidth : 0.7,
                lineWidth : 1
            },
            grid : {
                verticalLines : this.orientation,
                horizontalLines : !this.orientation,
                outline : "sw",
            },
            yaxis : {
                ticks: this.orientation ? data.ticks : false
            },
            xaxis : {
                labelsAngle: 45,
                ticks: this.orientation ? false : data.ticks
            }
        };
    },

    options_pie: function (data) {
        return {
            pie : {
                show: true
            },
            grid : {
                verticalLines : false,
                horizontalLines : false,
                outline : "",
            },
            xaxis :  {showLabels: false},
            yaxis :  {showLabels: false},
        };
    },

    options_radar: function (data) {
        return {
            radar : {
                show : true,
                stacked : this.stacked
            },
            grid : {
                circular : true,
                minorHorizontalLines : true
            },
            xaxis : {
                ticks: data.ticks
            },
        };
    },

    options_line: function (data) {
        return {
            lines : {
                show : true,
                stacked : this.stacked
            },
            grid : {
                verticalLines : this.orientation,
                horizontalLines : !this.orientation,
                outline : "sw",
            },
            yaxis : {
                ticks: this.orientation ? data.ticks : false
            },
            xaxis : {
                labelsAngle: 45,
                ticks: this.orientation ? false : data.ticks
            }
        };
    },

    graph_get_data: function () {
        return this.rpc('/web_graph/graph/data_get', {
            model: this.dataset.model,
            domain: this.domain,
            context: this.context,
            group_by: this.group_by,
            view_id: this.view_id,
            mode: this.mode,
            orientation: this.orientation,
            stacked: this.stacked
        });
    },

    // Render the graph and update menu styles
    graph_render: function (options) {
        options = options || {};
        _.extend(this, options.data);

        return this.graph_get_data()
            .then(this.proxy('graph_render_all'));
    },

    graph_render_all: function (data) {
        var i;
        if (this.mode=='area') {
            for (i=0; i<data.data.length; i++) {
                data.data[i].lines = {fill: true}
            }
        }
        if (this.graph) {
            this.graph.destroy();
        }

        // Render the graph
        this.$element.find(".graph_header_legend").children().remove();
        this.graph = this.make_graph(this.mode, this.container, data);

        // Update styles of menus

        this.$element.find("a[id^='graph_']").removeClass("active");
        this.$element.find("a[id='graph_"+this.mode+"']").addClass("active");
        this.$element.find("a[id='graph_"+this.mode+(this.stacked?"_stacked":"_not_stacked")+"']").addClass("active");

        if (this.legend == 'inside') {
            this.$element.find("a[id='graph_legend_inside']").addClass("active");
        } else if (this.legend == 'top') {
            this.$element.find("a[id='graph_legend_top']").addClass("active");
        } else {
            this.$element.find("a[id='graph_legend_no']").addClass("active");
        }

        if (this.spreadsheet) {
            this.$element.find("a[id='graph_show_data']").addClass("active");
        }
        return this.graph;
    },

    // render the graph using the domain, context and group_by
    // calls the 'graph_data_get' python controller to process all data
    // TODO: check is group_by should better be in the context
    do_search: function(domain, context, group_by) {
        this.domain = domain;
        this.context = context;
        this.group_by = group_by;

        this.graph_render();
    },

    do_show: function() {
        this.do_push_state({});
        return this._super();
    },
});
};
<|endoftext|>


<commit_before>// The main search module that allows users to view the Preference Terms Dictionary
//
// Requires Handlebars.js

(function ($, fluid) {
    "use strict";
    var search = fluid.registerNamespace("ctr.components.search");
    search.templates = { compiled: {} };
    // TODO:  Create session-scoped variables for query, status, record type, and language, and use if no data is provided.

    // Update the results displayed whenever we have new search data
    search.refresh = function(that) {
        // The query values are all stored in the form's DOM.
        // TODO: Review this with Antranig or Justin

        // TODO:  How do we pick up our base URL from the configuration?
        var settings = {
            url: "/api/search",
            data: {
                q: that.model.input.val()
            },
            success: displayResults,
            error:   displayError
        };
        // TODO:  Wire in support for status controls

        // TODO:  Wire in support for record type controls

        $.ajax(settings);
    };

    function displayError(jqXHR, textStatus, errorThrown) {
        prependTemplate("#main-viewport","error",{message: errorThrown});
    }

    function renderTemplate(key,context) {
        // templates are cached the first time they are used per page load
        var template = search.templates.compiled[key] ? search.templates.compiled[key] : Handlebars.compile($("#" + key).html());
        return template(context);
    }

    function replaceWithTemplate(el,key,context) {
        $(el).html(renderTemplate(key,context));
    }

    function appendTemplate(el,key,context) {
        $(el).append(renderTemplate(key,context));
    }

    function prependTemplate(el,key,context) {
        $(el).prepend(renderTemplate(key,context));
    }

    function displayResults(data, textStatus, jqXHR) {
        $("#main-viewport").html("");
        if (data && data.records && data.records.length > 0) {
            // prepend the control title bar
            // TODO:  Come up with a meaningful list of untranslated records
            appendTemplate("#main-viewport","navigation",{ count: data.records.length + 1 , untranslated: 0});

            // display each record in the results area
            data.records.forEach(function(record) {
                appendTemplate("#main-viewport","record",record);
            });
        }
        else {
            replaceWithTemplate("#main-viewport","norecord");
        }

        // TODO: add support for pagination or infinite scrolling
    }

    function loadTemplates(){
        var settings = {
          url: "/hbs",
          success: appendTemplates
        };
        $.ajax(settings);
    }

    function appendTemplates(data, textStatus, jqXHR) {
        $("body").append(data);
    }

    search.handleKeys = function(that, event) {
        if (event.which === 13) {
            search.refresh(that);
        }
    };

    loadTemplates();

    fluid.defaults("ctr.components.search", {
        gradeNames: ["fluid.viewComponent", "autoInit"],
        selectors: {
            "input":  ".ptd-search-input",
            "button": ".ptd-search-button",
            "results": ".ptd-search-results"
        },
        model: {
            "input": "{that}.dom.input",
            "statuses": [ "active", "unreviewed", "candidate"],
            "type": [ "record"]
        },
        members: {
            "input":  ".ptd-search-input",
            "button": ".ptd-search-button",
            "results": ".ptd-search-results"
        },
        events: {
            "refresh":  "preventable"
        },
        invokers: {
            "refresh": {
                funcName: "ctr.components.search.refresh",
                args: [ "{that}"]
            },
            "handleKeys": {
                funcName: "ctr.components.search.handleKeys",
                args: [ "{that}", "{arguments}.0"]
            }
        },
        listeners: {
            onCreate: [
                {
                    "this": "{that}.dom.button",
                    method: "click",
                    args: "{that}.refresh"
                },
                {
                    "this": "{that}.dom.input",
                    method: "keyup",
                    args: "{that}.handleKeys"
                }

            ],
            "refresh": {
                func: "ctr.components.search.refresh",
                args: [ "{that}"]
            }
        }
    });
})(jQuery, fluid_1_5);<commit_msg>Added support for using /api/records when there are no search terms.<commit_after>// The main search module that allows users to view the Preference Terms Dictionary
//
// Requires Handlebars.js

(function ($, fluid) {
    "use strict";
    var search = fluid.registerNamespace("ctr.components.search");
    search.templates = { compiled: {} };
    // TODO:  Create session-scoped variables for query, status, record type, and language, and use if no data is provided.

    // Update the results displayed whenever we have new search data
    search.refresh = function(that) {
        // The query values are all stored in the form's DOM.
        // TODO: Review this with Antranig or Justin

        var settings = {
            success: displayResults,
            error:   displayError
        };

        // TODO:  Wire in sorting and filtering to both types of requests
        if (that.model && that.model.input && that.model.input.val()) {
            settings.url = "/api/search";
            settings.data = { q: that.model.input.val()};
        }
        else {
            settings.url = "/api/records?children=true";
        }

        // TODO:  How do we pick up our base URL from the configuration?

        // TODO:  Wire in support for status controls

        // TODO:  Wire in support for record type controls

        $.ajax(settings);
    };

    function displayError(jqXHR, textStatus, errorThrown) {
        prependTemplate("#main-viewport","error",{message: errorThrown});
    }

    function renderTemplate(key,context) {
        // templates are cached the first time they are used per page load
        var template = search.templates.compiled[key] ? search.templates.compiled[key] : Handlebars.compile($("#" + key).html());
        return template(context);
    }

    function replaceWithTemplate(el,key,context) {
        $(el).html(renderTemplate(key,context));
    }

    function appendTemplate(el,key,context) {
        $(el).append(renderTemplate(key,context));
    }

    function prependTemplate(el,key,context) {
        $(el).prepend(renderTemplate(key,context));
    }

    function displayResults(data, textStatus, jqXHR) {
        $("#main-viewport").html("");
        if (data && data.records && data.records.length > 0) {
            // prepend the control title bar
            // TODO:  Come up with a meaningful list of untranslated records
            appendTemplate("#main-viewport","navigation",{ count: data.records.length + 1 , untranslated: 0});

            // display each record in the results area
            data.records.forEach(function(record) {
                appendTemplate("#main-viewport","record",record);
            });
        }
        else {
            replaceWithTemplate("#main-viewport","norecord");
        }

        // TODO: add support for pagination or infinite scrolling
    }

    function loadTemplates(){
        var settings = {
          url: "/hbs",
          success: appendTemplates
        };
        $.ajax(settings);
    }

    function appendTemplates(data, textStatus, jqXHR) {
        $("body").append(data);
    }

    search.handleKeys = function(that, event) {
        if (event.which === 13) {
            search.refresh(that);
        }
    };

    loadTemplates();

    fluid.defaults("ctr.components.search", {
        gradeNames: ["fluid.viewComponent", "autoInit"],
        selectors: {
            "input":  ".ptd-search-input",
            "button": ".ptd-search-button",
            "results": ".ptd-search-results"
        },
        model: {
            "input": "{that}.dom.input",
            "statuses": [ "active", "unreviewed", "candidate"],
            "type": [ "record"]
        },
        members: {
            "input":  ".ptd-search-input",
            "button": ".ptd-search-button",
            "results": ".ptd-search-results"
        },
        events: {
            "refresh":  "preventable"
        },
        invokers: {
            "refresh": {
                funcName: "ctr.components.search.refresh",
                args: [ "{that}"]
            },
            "handleKeys": {
                funcName: "ctr.components.search.handleKeys",
                args: [ "{that}", "{arguments}.0"]
            }
        },
        listeners: {
            onCreate: [
                {
                    "this": "{that}.dom.button",
                    method: "click",
                    args: "{that}.refresh"
                },
                {
                    "this": "{that}.dom.input",
                    method: "keyup",
                    args: "{that}.handleKeys"
                }

            ],
            "refresh": {
                func: "ctr.components.search.refresh",
                args: [ "{that}"]
            }
        }
    });
})(jQuery, fluid_1_5);<|endoftext|>