<commit_before>/*
    Framework for building object relational database apps
    Copyright (C) 2019  John Rogelstad

    This program 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.

    This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
*/
/*jslint this, browser*/
/**
    @module List
*/
import f from "../core.js";
import catalog from "./catalog.js";
import State from "../state.js";

const Qs = window.Qs;
const m = window.m;
const console = window.console;

const LIMIT = 20;

// ..........................................................
// PRIVATE
//

/**
    @class List
    @static
*/
function createList(feather) {
    let state;
    let doFetch;
    let doSave;
    let doSend;
    let onClean;
    let onDirty;
    let onDelete;
    let models = catalog.store().models();
    let name = feather.toCamelCase();
    let isSubscribed = false;
    let ary = [];
    let dirty = [];
    let sid = f.createId();
    let isCheckUpdates = false;

    // ..........................................................
    // PUBLIC
    //

    /**
        Add a model to the list. Will replace existing
        if model with same id is already found in array.
        Pass subscribe option to automatically subscribe
        to server events on the model.

        @method add
        @param {Model} Model
        @param {Boolean} Subscribe flag.
    */
    ary.add = function (model, subscribe) {
        let mstate;
        let payload;
        let url;
        let query;
        let subid;
        let id = model.id();
        let idx = ary.index();
        let oid = Number(idx[id]);

        if (!Number.isNaN(oid)) {
            dirty.remove(ary[oid]);
            ary.splice(oid, 1, model);
        } else {
            idx[id] = ary.length;
            ary.push(model);
        }

        mstate = model.state();
        mstate.resolve("/Delete").enter(onDirty.bind(model));
        mstate.resolve("/Ready/Fetched/Dirty").enter(onDirty.bind(model));
        mstate.resolve("/Ready/Fetched/Clean").enter(onClean.bind(model));
        mstate.resolve("/Deleted").enter(onDelete.bind(model));

        if (model.state().current()[0] === "/Ready/New") {
            dirty.push(model);
            state.send("changed");
        }

        // Subscribe to events on new model if applicable
        if (subscribe) {
            subid = ary.subscribe();

            if (!subid) {
                return;
            }

            query = Qs.stringify({
                id: model.id(),
                subscription: {
                    id: subid,
                    eventKey: catalog.eventKey(),
                    merge: true
                }
            });
            url = "/do/subscribe/" + query;
            payload = {
                method: "POST",
                url: url
            };

            m.request(payload).catch(console.error);
        }
    };

    /**
        Set whether the array can have a filter. Turn
        off when list is not fetching.

        @method canFilter
        @param {Boolean} Can filter flag
        @return {Boolean}
    */
    ary.canFilter = f.prop(true);

    /**
        If turned on perform `checkUpdate` on all fetched
        models, or any newly fetched models. Do this if
        models are going to be edited.

        Note: only functions when `isEditable` is true.

        @method checkUpdate
        @param {Boolean} Enable or disable checking
    */
    ary.checkUpdate = function (enabled) {
        if (enabled === true) {
            isCheckUpdates = true;
            ary.forEach(function (model) {
                model.checkUpdate();
            });
        } else if (enabled === false) {
            isCheckUpdates = false;
        }
    };

    /**
        Fetch data. Returns a Promise.

        @method fetch
        @param {Filter} filter
        @param {Boolean} merge
        @param {Array} properties
        @return {Object}
    */
    ary.fetch = function (filter, merge) {
        ary.filter(filter || {});

        return doSend("fetch", merge);
    };

    /**
        Filter to use when fetching.

        @method filter
        @param {Filter} Filter
        @return {Filter}
    */
    ary.filter = f.prop({});

    /**
        Default fetch limit.

        @method defaultLimit
        @param {Integer} Limit
        @return {Integer}
    */
    ary.defaultLimit = f.prop(LIMIT);

    /**
        Model index.

        @method index
        @param {Object} Model index
        @return {Object}
    */
    ary.index = f.prop({});

    /**
        Flag whether data will be editable. If false
        models in list will be instantiated generically
        meaning no model specific business logic will
        be applied. It enables queries on specific
        properties, all of which improves performance.
        Models will also be set as read only.

        @method properties
        @param {Boolean} flag
        @return {Array}
    */
    ary.isEditable = f.prop(true);

    /**
        Model factory for creating new model instances.

        @method model
        @param {Object} Data
        @param {Object} Feather
        @return {Objects}
    */
    ary.model = models[feather.toCamelCase() || "Model"];

    /**
        The url path to data.

        @method path
        @param {String} Path
        @return {String}
    */
    ary.path = f.prop();

    /**
        Array of properties to fetch if only a subset required.
        If undefined, then all properties returned.

        @method properties
        @param {Array} Array of properties
        @return {Array}
    */
    ary.properties = f.prop();

    /**
        Remove a model from the list.

        @method remove
        @param {Model} Model
    */
    ary.remove = function (model) {
        let id = model.id();
        let idx = ary.index();
        let i = Number(idx[id]);

        if (!Number.isNaN(i)) {
            ary.splice(i, 1);
            Object.keys(idx).forEach(function (key) {
                if (idx[key] > i) {
                    idx[key] -= 1;
                }
            });
            delete idx[id];
        }
        dirty.remove(model);
    };

    /**
        Clear list.

        @method reset
    */
    ary.reset = function () {
        ary.length = 0;
        dirty.length = 0;
        ary.index({});
    };

    /**
        Toggle whether to show deleted records.

        @method showDeleted
        @param {Boolean} Whether to show deleted
        @return {Boolean}
    */
    ary.showDeleted = f.prop(false);

    /**
        Save dirty records in list. Returns a Promise.

        @method save
        @return {Promise}
    */
    ary.save = function () {
        return doSend("save");
    };

    /**
        List state.

        @method state
        @return {State}
    */
    ary.state = function () {
        return state;
    };

    /**
        Subscribe to change events on any records
        in the array. Returns subscription id when
        enabled by passing true at least once. Pass
        false to unsubscribe.

        @method subscribe
        @param {Boolean} Subscribe or unsubscribe.
        @return {String} Subcription id.
    */
    ary.subscribe = function (...args) {
        let query;
        let url;
        let payload;

        if (args.length) {
            if (args[0] === true) {
                isSubscribed = true;
                catalog.register("subscriptions", sid, ary);
            } else {
                if (isSubscribed) {
                    catalog.unregister("subscriptions", sid);

                    // Let the server know we're unsubscribing
                    query = {
                        subscription: {
                            id: sid
                        }
                    };

                    query = Qs.stringify(query);
                    url = "/do/unsubscribe/" + query;
                    payload = {
                        method: "POST",
                        url: url
                    };

                    return m.request(payload).catch(console.error);
                }

                isSubscribed = false;
            }
        }

        return (
            isSubscribed
            ? sid
            : false
        );
    };

    // ..........................................................
    // PRIVATE
    //

    onClean = function () {
        dirty.remove(this);
        state.send("changed");
    };

    onDelete = function () {
        ary.remove(this);
        state.send("changed");
    };

    onDirty = function () {
        dirty.push(this);
        state.send("changed");
    };

    dirty.remove = function (model) {
        let i = dirty.indexOf(model);

        if (i > -1) {
            dirty.splice(i, 1);
        }
    };

    doFetch = function (context) {
        let url;
        let payload;
        let subid = ary.subscribe();
        let body = {};
        let merge = true;
        let cfeather = catalog.getFeather(name.toCamelCase(true));

        // Undo any edited rows
        ary.forEach(function (model) {
            model.undo();
        });

        if (context.merge === false) {
            merge = false;
        }

        function callback(data) {
            let attrs;
            let props = {};
            let cache = [];

            if (!merge) {
                ary.reset();
            }

            if (ary.isEditable()) {
                data.forEach(function (item) {
                    let model = models[name](item);

                    model.state().goto("/Ready/Fetched");
                    if (isCheckUpdates) {
                        model.checkUpdate();
                    }
                    ary.add(model);
                });
            } else {
                // Strip dot notation
                if (body.properties) {
                    body.properties = body.properties.map(function (p) {
                        let i = p.indexOf(".");
                        let ret = p;

                        if (i !== -1) {
                            ret = p.slice(0, i);
                            if (cache.indexOf(ret) !== -1) {
                                ret = undefined;
                            }
                            cache.push(ret);
                        }
                        cache.push(ret);
                        return ret;
                    }).filter(function (p) {
                        return p !== undefined;
                    });
                }

                attrs = (
                    body.properties || Object.keys(cfeather.properties)
                );

                Object.keys(cfeather.properties).forEach(function (attr) {
                    let prop = cfeather.properties[attr];

                    if (prop.isAlwaysLoad || attrs.indexOf(attr) > -1) {
                        props[attr] = prop;
                    }
                });

                cfeather.properties = props;
                data.forEach(function (item) {
                    let model = models[name](item, cfeather);

                    model.state().goto("/Ready/Fetched");
                    ary.add(model);
                });
            }

            state.send("fetched");
            context.resolve(ary);
        }

        if (ary.properties()) {
            body.properties = ary.properties();
            // Make sure required properties are included
            Object.keys(cfeather.properties).forEach(function (key) {
                if (
                    cfeather.properties[key].isAlwaysLoad &&
                    body.properties.indexOf(key) === -1
                ) {
                    body.properties.push(key);
                }
            });
        }

        if (ary.filter()) {
            body.filter = ary.filter();
            body.filter.limit = body.filter.limit || ary.defaultLimit();
        }

        body.showDeleted = ary.showDeleted();
        if (subid) {
            body.subscription = {
                id: subid,
                eventKey: catalog.eventKey(),
                merge: merge
            };
        }

        url = ary.path();
        payload = {
            method: "POST",
            url: url,
            body: body
        };

        return m.request(payload).then(callback).catch(console.error);
    };

    doSave = function (context) {
        let requests = [];

        dirty.forEach(function (model) {
            requests.push(model.save());
        });

        Promise.all(requests).then(function (resp) {
            state.send("changed");
            context.resolve(resp);
        }).catch(context.reject);
    };

    doSend = function (...args) {
        let evt = args[0];
        let merge = args[1];

        return new Promise(function (resolve, reject) {
            let context = {
                resolve: resolve,
                reject: reject
            };

            if (args.length > 1) {
                context.merge = merge;
            }

            state.send(evt, context);
        });
    };

    // Define statechart
    state = State.define(function () {
        this.state("Unitialized", function () {
            this.event("fetch", function (context) {
                this.goto("/Busy", {
                    context: context
                });
            });
        });

        this.state("Busy", function () {
            this.state("Fetching", function () {
                this.enter(doFetch);
            });
            this.state("Saving", function () {
                this.enter(doSave);
                this.event("changed", function () {
                    this.goto("/Fetched");
                });
                this.canExit = function () {
                    return !dirty.length;
                };
            });
            this.event("fetched", function () {
                this.goto("/Fetched");
            });
        });

        this.state("Fetched", function () {
            this.event("changed", function () {
                this.goto("/Fetched", {
                    force: true
                });
            });
            this.c = this.C; // Squelch jslint complaint
            this.c(function () {
                if (dirty.length) {
                    return "./Dirty";
                }
                return "./Clean";
            });
            this.event("fetch", function (context) {
                this.goto("/Busy", {
                    context: context
                });
            });
            this.state("Clean", function () {
                this.enter(function () {
                    dirty.length = 0;
                });
            });
            this.state("Dirty", function () {
                this.event("save", function (context) {
                    this.goto("/Busy/Saving", {
                        context: context
                    });
                });
            });
        });
    });
    state.goto();

    return ary;
}

function list(feather) {
    // Instantiate the list, optionally auto fetch
    // and return a property that contains the array.
    return function (options) {
        options = options || {};
        let plural;
        let ary = options.value || createList(feather);
        let prop = f.prop(ary);

        if (options.path) {
            ary.path(options.path);
        } else {
            plural = catalog.getFeather(feather).plural.toSpinalCase();
            ary.path("/data/" + plural);
        }

        ary.showDeleted(options.showDeleted === true);
        ary.subscribe(options.subscribe === true);
        ary.isEditable(options.isEditable !== false);

        if (options.fetch !== false) {
            ary.fetch(options.filter, options.merge);
        } else {
            ary.filter(options.filter || {});
        }

        return prop;
    };
}

catalog.register("factories", "list", list);

export default Object.freeze(list);<commit_msg>Update list.js<commit_after>/*
    Framework for building object relational database apps
    Copyright (C) 2019  John Rogelstad

    This program 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.

    This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
*/
/*jslint this, browser*/
/**
    @module List
*/
import f from "../core.js";
import catalog from "./catalog.js";
import State from "../state.js";

const Qs = window.Qs;
const m = window.m;
const console = window.console;

const LIMIT = 20;

// ..........................................................
// PRIVATE
//

/**
    @class List
    @static
*/
function createList(feather) {
    let state;
    let doFetch;
    let doSave;
    let doSend;
    let onClean;
    let onDirty;
    let onDelete;
    let models = catalog.store().models();
    let name = feather.toCamelCase();
    let isSubscribed = false;
    let ary = [];
    let dirty = [];
    let sid = f.createId();
    let isCheckUpdates = false;

    // ..........................................................
    // PUBLIC
    //

    /**
        Add a model to the list. Will replace existing
        if model with same id is already found in array.
        Pass subscribe option to automatically subscribe
        to server events on the model.

        @method add
        @param {Model} Model
        @param {Boolean} Subscribe flag.
    */
    ary.add = function (model, subscribe) {
        let mstate;
        let payload;
        let url;
        let query;
        let subid;
        let id = model.id();
        let idx = ary.index();
        let oid = Number(idx[id]);

        if (!Number.isNaN(oid)) {
            dirty.remove(ary[oid]);
            ary.splice(oid, 1, model);
        } else {
            idx[id] = ary.length;
            ary.push(model);
        }

        mstate = model.state();
        mstate.resolve("/Delete").enter(onDirty.bind(model));
        mstate.resolve("/Ready/Fetched/Dirty").enter(onDirty.bind(model));
        mstate.resolve("/Ready/Fetched/Clean").enter(onClean.bind(model));
        mstate.resolve("/Deleted").enter(onDelete.bind(model));

        if (model.state().current()[0] === "/Ready/New") {
            dirty.push(model);
            state.send("changed");
        }

        // Subscribe to events on new model if applicable
        if (subscribe) {
            subid = ary.subscribe();

            if (!subid) {
                return;
            }

            query = Qs.stringify({
                id: model.id(),
                subscription: {
                    id: subid,
                    eventKey: catalog.eventKey(),
                    merge: true
                }
            });
            url = "/do/subscribe/" + query;
            payload = {
                method: "POST",
                url: url
            };

            m.request(payload).catch(console.error);
        }
    };

    /**
        Set whether the array can have a filter. Turn
        off when list is not fetching.

        @method canFilter
        @param {Boolean} Can filter flag
        @return {Boolean}
    */
    ary.canFilter = f.prop(true);

    /**
        If turned on perform `checkUpdate` on all fetched
        models, or any newly fetched models. Do this if
        models are going to be edited.

        Note: only functions when `isEditable` is true.

        @method checkUpdate
        @param {Boolean} Enable or disable checking
    */
    ary.checkUpdate = function (enabled) {
        if (enabled === true) {
            isCheckUpdates = true;
            ary.forEach(function (model) {
                model.checkUpdate();
            });
        } else if (enabled === false) {
            isCheckUpdates = false;
        }
    };

    /**
        Fetch data. Returns a Promise.

        @method fetch
        @param {Filter} filter
        @param {Boolean} merge
        @param {Array} properties
        @return {Object}
    */
    ary.fetch = function (filter, merge) {
        ary.filter(filter || {});

        return doSend("fetch", merge);
    };

    /**
        Filter to use when fetching.

        @method filter
        @param {Filter} Filter
        @return {Filter}
    */
    ary.filter = f.prop({});

    /**
        Default fetch limit.

        @method defaultLimit
        @param {Integer} Limit
        @return {Integer}
    */
    ary.defaultLimit = f.prop(LIMIT);

    /**
        Model index.

        @method index
        @param {Object} Model index
        @return {Object}
    */
    ary.index = f.prop({});

    /**
        Flag whether data will be editable. If false
        models in list will be instantiated generically
        meaning no model specific business logic will
        be applied. It enables queries on specific
        properties, all of which improves performance.
        Models will also be set as read only.

        @method properties
        @param {Boolean} flag
        @return {Array}
    */
    ary.isEditable = f.prop(true);

    /**
        Model factory for creating new model instances.

        @method model
        @param {Object} Data
        @param {Object} Feather
        @return {Objects}
    */
    ary.model = models[feather.toCamelCase() || "Model"];

    /**
        The url path to data.

        @method path
        @param {String} Path
        @return {String}
    */
    ary.path = f.prop();

    /**
        Array of properties to fetch if only a subset required.
        If undefined, then all properties returned.

        @method properties
        @param {Array} Array of properties
        @return {Array}
    */
    ary.properties = f.prop();

    /**
        Remove a model from the list.

        @method remove
        @param {Model} Model
    */
    ary.remove = function (model) {
        let id = model.id();
        let idx = ary.index();
        let i = Number(idx[id]);

        if (!Number.isNaN(i)) {
            ary.splice(i, 1);
            Object.keys(idx).forEach(function (key) {
                if (idx[key] > i) {
                    idx[key] -= 1;
                }
            });
            delete idx[id];
        }
        dirty.remove(model);
    };

    /**
        Clear list.

        @method reset
    */
    ary.reset = function () {
        ary.length = 0;
        dirty.length = 0;
        ary.index({});
    };

    /**
        Toggle whether to show deleted records.

        @method showDeleted
        @param {Boolean} Whether to show deleted
        @return {Boolean}
    */
    ary.showDeleted = f.prop(false);

    /**
        Save dirty records in list. Returns a Promise.

        @method save
        @return {Promise}
    */
    ary.save = function () {
        return doSend("save");
    };

    /**
        List state.

        @method state
        @return {State}
    */
    ary.state = function () {
        return state;
    };

    /**
        Subscribe to change events on any records
        in the array. Returns subscription id when
        enabled by passing true at least once. Pass
        false to unsubscribe.

        @method subscribe
        @param {Boolean} Subscribe or unsubscribe.
        @return {String} Subcription id.
    */
    ary.subscribe = function (...args) {
        let query;
        let url;
        let payload;

        if (args.length) {
            if (args[0] === true) {
                isSubscribed = true;
                catalog.register("subscriptions", sid, ary);
            } else {
                if (isSubscribed) {
                    catalog.unregister("subscriptions", sid);

                    // Let the server know we're unsubscribing
                    query = {
                        subscription: {
                            id: sid
                        }
                    };

                    query = Qs.stringify(query);
                    url = "/do/unsubscribe/" + query;
                    payload = {
                        method: "POST",
                        url: url
                    };

                    return m.request(payload).catch(console.error);
                }

                isSubscribed = false;
            }
        }

        return (
            isSubscribed
            ? sid
            : false
        );
    };

    // ..........................................................
    // PRIVATE
    //

    onClean = function () {
        dirty.remove(this);
        state.send("changed");
    };

    onDelete = function () {
        ary.remove(this);
        state.send("changed");
    };

    onDirty = function () {
        dirty.push(this);
        state.send("changed");
    };

    dirty.remove = function (model) {
        let i = dirty.indexOf(model);

        if (i > -1) {
            dirty.splice(i, 1);
        }
    };

    doFetch = function (context) {
        let url;
        let payload;
        let subid = ary.subscribe();
        let body = {};
        let merge = true;
        let cfeather = catalog.getFeather(name.toCamelCase(true));

        // Undo any edited rows
        ary.forEach(function (model) {
            model.undo();
        });

        if (context.merge === false) {
            merge = false;
        }

        function callback(data) {
            let attrs;
            let props = {};
            let cache = [];

            if (!merge) {
                ary.reset();
            }

            if (ary.isEditable()) {
                data.forEach(function (item) {
                    let model = models[name](item);

                    model.state().goto("/Ready/Fetched");
                    if (isCheckUpdates) {
                        model.checkUpdate();
                    }
                    ary.add(model);
                });
            } else {
                // Strip dot notation
                if (body.properties) {
                    body.properties = body.properties.map(function (p) {
                        let i = p.indexOf(".");
                        let ret = p;

                        if (i !== -1) {
                            ret = p.slice(0, i);
                            if (cache.indexOf(ret) !== -1) {
                                ret = undefined;
                            }
                            cache.push(ret);
                        }
                        cache.push(ret);
                        return ret;
                    }).filter(function (p) {
                        return p !== undefined;
                    });
                }

                attrs = (
                    body.properties || Object.keys(cfeather.properties)
                );

                Object.keys(cfeather.properties).forEach(function (attr) {
                    let prop = cfeather.properties[attr];

                    if (prop.isAlwaysLoad || attrs.indexOf(attr) > -1) {
                        props[attr] = prop;
                    }
                });

                cfeather.properties = props;
                data.forEach(function (item) {
                    let model = models[name](item, cfeather);

                    model.state().goto("/Ready/Fetched");
                    ary.add(model);
                });
            }

            state.send("fetched");
            context.resolve(ary);
        }

        if (ary.properties()) {
            body.properties = ary.properties();
            // Make sure required properties are included
            Object.keys(cfeather.properties).forEach(function (key) {
                if (
                    cfeather.properties[key].isAlwaysLoad &&
                    body.properties.indexOf(key) === -1
                ) {
                    body.properties.push(key);
                }
            });
        }

        if (ary.filter()) {
            body.filter = ary.filter();
            body.filter.limit = body.filter.limit || ary.defaultLimit();
        }

        body.showDeleted = ary.showDeleted();
        if (subid) {
            body.subscription = {
                id: subid,
                eventKey: catalog.eventKey(),
                merge: merge
            };
        }

        url = ary.path();
        payload = {
            method: "POST",
            url: url,
            body: body
        };

        return m.request(payload).then(callback).catch(console.error);
    };

    doSave = function (context) {
        let requests = [];

        dirty.forEach(function (model) {
            requests.push(model.save());
        });

        Promise.all(requests).then(function (resp) {
            state.send("changed");
            context.resolve(resp);
        }).catch(context.reject);
    };

    doSend = function (...args) {
        let evt = args[0];
        let merge = args[1];

        return new Promise(function (resolve, reject) {
            let context = {
                resolve: resolve,
                reject: reject
            };

            if (args.length > 1) {
                context.merge = merge;
            }

            state.send(evt, context);
        });
    };

    // Define statechart
    state = State.define(function () {
        this.state("Unitialized", function () {
            this.event("fetch", function (context) {
                this.goto("/Busy", {
                    context: context
                });
            });
        });

        this.state("Busy", function () {
            this.state("Fetching", function () {
                this.enter(doFetch);
            });
            this.state("Saving", function () {
                this.enter(doSave);
                this.event("changed", function () {
                    this.goto("/Fetched");
                });
            });
            this.event("fetched", function () {
                this.goto("/Fetched");
            });
        });

        this.state("Fetched", function () {
            this.event("changed", function () {
                this.goto("/Fetched", {
                    force: true
                });
            });
            this.c = this.C; // Squelch jslint complaint
            this.c(function () {
                if (dirty.length) {
                    return "./Dirty";
                }
                return "./Clean";
            });
            this.event("fetch", function (context) {
                this.goto("/Busy", {
                    context: context
                });
            });
            this.state("Clean", function () {
                this.enter(function () {
                    dirty.length = 0;
                });
            });
            this.state("Dirty", function () {
                this.event("save", function (context) {
                    this.goto("/Busy/Saving", {
                        context: context
                    });
                });
            });
        });
    });
    state.goto();

    return ary;
}

function list(feather) {
    // Instantiate the list, optionally auto fetch
    // and return a property that contains the array.
    return function (options) {
        options = options || {};
        let plural;
        let ary = options.value || createList(feather);
        let prop = f.prop(ary);

        if (options.path) {
            ary.path(options.path);
        } else {
            plural = catalog.getFeather(feather).plural.toSpinalCase();
            ary.path("/data/" + plural);
        }

        ary.showDeleted(options.showDeleted === true);
        ary.subscribe(options.subscribe === true);
        ary.isEditable(options.isEditable !== false);

        if (options.fetch !== false) {
            ary.fetch(options.filter, options.merge);
        } else {
            ary.filter(options.filter || {});
        }

        return prop;
    };
}

catalog.register("factories", "list", list);

export default Object.freeze(list);<|endoftext|>


<commit_before>var convert = require('convert');
var model = require('model');
var defaults = require('model-defaults');
var session = require('session');
var each = require('each');

/**
 * MPS to MPH
 */

var MPS_TO_MPH = 2.23694;

/**
 * Expose `Route`
 */

var Route = module.exports = model('Route')
  .use(defaults({
    costPenalty: false,
    costSavings: false,
    modes: [],
    timeSavings: false,
    transit: [],
    weightLost: false
  }))
  .attr('id')
  .attr('access')
  .attr('bikeCalories')
  .attr('bikeDistance')
  .attr('bikeTime')
  .attr('calories')
  .attr('cost')
  .attr('costPenalty')
  .attr('costSavings')
  .attr('driveDistance')
  .attr('egress')
  .attr('emissions')
  .attr('emissionsDifference')
  .attr('hasTransit')
  .attr('modes')
  .attr('score')
  .attr('stats')
  .attr('summary')
  .attr('time')
  .attr('timeSavings')
  .attr('transfers')
  .attr('transitCost')
  .attr('transit')
  .attr('trips')
  .attr('walkCalories')
  .attr('walkDistance')
  .attr('walkTime')
  .attr('weightLost');

/**
 * Changes to emit on rescore
 */

var emitAfterRescore = ['average', 'bikeTime', 'calculatedCost', 'calculatedCalories', 'transitCosts', 'tripsPerYear',
  'carParkingCost', 'vmtRate', 'walkTime'
];

/**
 * Update scoring
 */

Route.prototype.rescore = function(scorer) {
  var data = scorer.processOption(this.toJSON());

  for (var i in data) {
    if (this.hasOwnProperty(i) && i !== 'transitCost') {
      this[i](data[i]);
    }
  }

  for (i in emitAfterRescore) {
    var attr = emitAfterRescore[i];
    this.emit('change ' + attr, this[attr]());
  }
};

/**
 * Set car data
 */

Route.prototype.setCarData = function(data) {
  var m = this.tripm();

  var costDifference = data.cost * m - this.cost() * m;
  var emissions = (data.emissions - this.emissions()) / data.emissions * 100;
  var timeSavings = (this.timeInTransit() - (data.time - this.time())) * m;

  if (this.directCar()) {
    costDifference = data.cost * m / 2;
    emissions = 50;
    timeSavings = this.average() * m / 2; // Assume split driving
  }

  if (costDifference > 0) {
    this.costSavings(costDifference);
  }

  if (this.calories() !== 0) {
    this.weightLost(parseInt(convert.caloriesToPounds(this.calories()) * m, 10));
  }

  if (timeSavings > 60) {
    this.timeSavings(parseInt(timeSavings / 60 / 60, 10));
  }

  if (emissions > 0) {
    this.emissionsDifference(parseInt(emissions, 10));
  }
};

/**
 * Direct car?
 */

Route.prototype.directCar = function() {
  return this.modes().length === 1 && this.hasCar();
};

/**
 * Is this a direct bike or walk journey?
 */

Route.prototype.directBikeOrWalk = function() {
  return !this.hasTransit() && !this.hasCar();
};

/**
 * Average trip length in minutes
 */

Route.prototype.average = function() {
  if (this.hasTransit() || this.modes().indexOf('car') === -1) {
    return Math.round(this.time() / 60);
  } else {
    return Math.round(this.time() / 60 * 1.35);
  }
};

/**
 * Freeflow
 */

Route.prototype.freeflowTime = function() {
  if (this.hasTransit() || this.modes().indexOf('car') === -1) {
    return false;
  } else {
    return Math.round(this.time() / 60);
  }
};

/**
 * Time in transit
 */

Route.prototype.timeInTransit = function() {
  if (!this.hasTransit()) {
    return 0;
  } else {
    return this.transit().reduce(function(m, t) {
      return m + t.waitStats.avg + t.rideStats.avg;
    }, 0) / 60;
  }
};

/**
 * Shorthand helpers
 */

Route.prototype.hasCost = function() {
  return this.cost() > 0;
};

Route.prototype.hasCar = function() {
  return this.modes().indexOf('car') !== -1;
};

Route.prototype.hasTransit = function() {
  return this.transit().length > 0;
};

Route.prototype.hasBiking = function() {
  return this.modes().indexOf('bicycle') !== -1;
};

Route.prototype.hasWalking = function() {
  return this.modes().indexOf('walk') !== -1;
};

/**
 * Days
 */

Route.prototype.tripsPerYear = function() {
  return session.plan().tripsPerYear();
};

/**
 * Trip multiplier
 */

Route.prototype.tripm = function() {
  return session.plan().tripsPerYear();
};

/**
 * Cost
 */

Route.prototype.calculatedCost = function() {
  if (this.cost() === 0) {
    return false;
  }

  var cost = 0;
  if (this.transitCost()) {
    cost += this.transitCost();
  }
  if (this.modes().indexOf('car') !== -1) {
    cost += this.vmtRate() * this.driveDistances();
    cost += this.carParkingCost();
  }

  var total = cost * this.tripm();
  if (total > 100) {
    return parseInt(total, 10);
  } else {
    return total.toFixed(2);
  }
};

/**
 * Transit Cost
 */

Route.prototype.transitCosts = function() {
  if (!this.transitCost()) {
    return false;
  } else {
    return this.transitCost().toFixed(2);
  }
};

/**
 * Total Calories
 */

Route.prototype.totalCalories = function() {
  if (this.walkDistances() === 0 && this.bikeDistances() === 0) return 0;

  var cals = walkingCaloriesBurned(this.walkSpeed(), this.weight(), this.walkDistance() / this.walkSpeed() / 60 / 60);
  if (this.modes().indexOf('bicycle') !== -1) {
    cals += bikingCaloriesBurned(this.bikeSpeed(), this.weight(), this.bikeDistance() / this.bikeSpeed() / 60 / 60);
  }

  return cals;
};

/**
 * Frequency
 */

Route.prototype.frequency = function() {
  var trips = this.trips();
  if (!trips) {
    return false;
  }

  var plan = session.plan();
  var start = plan.start_time();
  var end = plan.end_time();

  return Math.round(60 / (trips / (end - start)));
};

/**
 * Walk/Bike distances rounded
 */

Route.prototype.driveDistances = function() {
  return this.distances('car', 'driveDistance');
};

Route.prototype.bikeDistances = function() {
  return this.distances('bicycle', 'bikeDistance');
};

Route.prototype.walkDistances = function() {
  return this.distances('walk', 'walkDistance');
};

Route.prototype.distances = function(mode, val) {
  if (this.modes().indexOf(mode) === -1) {
    return false;
  } else {
    return convert.metersToMiles(this[val]());
  }
};

/**
 * Walk/bike speed in MPH
 */

Route.prototype.bikeSpeedMph = function() {
  return toFixed(this.bikeSpeed() * MPS_TO_MPH, 1);
};

Route.prototype.walkSpeedMph = function() {
  return toFixed(this.walkSpeed() * MPS_TO_MPH, 1);
};

/**
 * Walk/bike time in minutes
 */

Route.prototype.bikeTime = function() {
  return timeFromSpeedAndDistance(this.bikeSpeed(), this.bikeDistance());
};

Route.prototype.walkTime = function() {
  return timeFromSpeedAndDistance(this.walkSpeed(), this.walkDistance());
};

function timeFromSpeedAndDistance(s, d) {
  var t = d / s;
  if (t < 60) {
    return '< 1';
  } else {
    return parseInt(t / 60, 10);
  }
}

/**
 * Retrieve from scorer
 */

Route.prototype.bikeSpeed = function() {
  return session.plan().scorer().rates.bikeSpeed;
};

Route.prototype.walkSpeed = function() {
  return session.plan().scorer().rates.walkSpeed;
};

Route.prototype.vmtRate = function() {
  return session.plan().scorer().rates.mileageRate;
};

Route.prototype.weight = function() {
  return session.plan().scorer().rates.weight;
};

Route.prototype.carParkingCost = function() {
  return session.plan().scorer().rates.carParkingCost;
};

/**
 * Construct a simple mode-based descriptor (e.g. "Drive to Transit")
 */

Route.prototype.modeDescriptor = function() {
  var modeStr;

  if (this.bikeDistance() > 0) {
    modeStr = 'bike';
  } else if (this.driveDistance() > 0) {
    modeStr = 'drive';
  } else {
    modeStr = 'walk';
  }

  if (this.hasTransit()) {
    modeStr += ' to transit';
  } else if (this.driveDistance() > 0) {
    modeStr = 'rideshare';
  }

  return modeStr;
};

/**
 * Walking Calories
 *
 * CB = [0.0215 x KPH3 - 0.1765 x KPH2 + 0.8710 x KPH + 1.4577] x WKG x T
 * http://www.shapesense.com/fitness-exercise/calculators/walking-calorie-burn-calculator.aspx
 */

function walkingCaloriesBurned(mps, wkg, hours) {
  var kph = mps / 1000 * 60 * 60;
  var kph2 = kph * kph;
  var kph3 = kph2 * kph;
  return (0.0215 * kph3 - 0.1765 * kph2 + 0.8710 * kph) * wkg * hours;
}

/**
 * Biking Calories
 *
 * http://en.wikipedia.org/wiki/Bicycle_performance
 */

var GRADE = 1;
var GRAVITY = 9.8;
var K1 = 0.0053; // frictional losses
var K2 = 0.185; // aerodynamic drag
var WATTS_TO_CALS_PER_SECOND = 0.2388;

function bikingCaloriesBurnedOld(mps, wkg, hours) {
  var mps3 = Math.pow(mps, 3);
  var seconds = hours * 60 * 60;
  var watts = GRAVITY * wkg * mps * (K1 + GRADE) + K2 * mps3;
  return watts * WATTS_TO_CALS_PER_SECOND * seconds;
}

function bikingCaloriesBurned(mps, wkg, hours) {
  return 8 * wkg * hours;
}

function toFixed(n, f) {
  var m = Math.pow(10, f);
  return ((n * m) | 0) / m;
}

/**
 * Tags
 */

Route.prototype.tags = function(plan) {
  var tags = [];

  // add the access mode tags
  each(this.get('access'), function(accessLeg) {
    tags.push(accessLeg.mode);
  });

  // add tags for each transit leg
  each(this.transit(), function(transitLeg) {
    tags.push(transitLeg.mode); // add the transit mode tag
    if (transitLeg.routes.length > 0) { //add the agency tag
      tags.push(transitLeg.routes[0].id.split(':')[0]);
    }
  });

  // add a generic 'transit' tag
  if (this.hasTransit()) tags.push('transit');

  // add tags for the from/to locations
  if (plan) {
    var from = locationToTags(plan.from());
    var to = locationToTags(plan.to());
    tags = tags.concat(from).concat(to);
  }

  tags = tags.map(function(tag) {
    return tag.toLowerCase().trim();
  });
  return tags;
};

function locationToTags(location) {
  // strip off the zip code, if present
  var endsWithZip = /\d{5}$/;
  if (endsWithZip.test(location)) {
    location = location.substring(0, location.length - 5);
  }
  return location.split(',').slice(1);
}
<commit_msg>Fix mode descriptor<commit_after>var convert = require('convert');
var model = require('model');
var defaults = require('model-defaults');
var session = require('session');
var each = require('each');

/**
 * MPS to MPH
 */

var MPS_TO_MPH = 2.23694;

/**
 * Expose `Route`
 */

var Route = module.exports = model('Route')
  .use(defaults({
    costPenalty: false,
    costSavings: false,
    modes: [],
    timeSavings: false,
    transit: [],
    weightLost: false
  }))
  .attr('id')
  .attr('access')
  .attr('bikeCalories')
  .attr('bikeDistance')
  .attr('bikeTime')
  .attr('calories')
  .attr('cost')
  .attr('costPenalty')
  .attr('costSavings')
  .attr('driveDistance')
  .attr('egress')
  .attr('emissions')
  .attr('emissionsDifference')
  .attr('hasTransit')
  .attr('modes')
  .attr('score')
  .attr('stats')
  .attr('summary')
  .attr('time')
  .attr('timeSavings')
  .attr('transfers')
  .attr('transitCost')
  .attr('transit')
  .attr('trips')
  .attr('walkCalories')
  .attr('walkDistance')
  .attr('walkTime')
  .attr('weightLost');

/**
 * Changes to emit on rescore
 */

var emitAfterRescore = ['average', 'bikeTime', 'calculatedCost', 'calculatedCalories', 'transitCosts', 'tripsPerYear',
  'carParkingCost', 'vmtRate', 'walkTime'
];

/**
 * Update scoring
 */

Route.prototype.rescore = function(scorer) {
  var data = scorer.processOption(this.toJSON());

  for (var i in data) {
    if (this.hasOwnProperty(i) && i !== 'transitCost') {
      this[i](data[i]);
    }
  }

  for (i in emitAfterRescore) {
    var attr = emitAfterRescore[i];
    this.emit('change ' + attr, this[attr]());
  }
};

/**
 * Set car data
 */

Route.prototype.setCarData = function(data) {
  var m = this.tripm();

  var costDifference = data.cost * m - this.cost() * m;
  var emissions = (data.emissions - this.emissions()) / data.emissions * 100;
  var timeSavings = (this.timeInTransit() - (data.time - this.time())) * m;

  if (this.directCar()) {
    costDifference = data.cost * m / 2;
    emissions = 50;
    timeSavings = this.average() * m / 2; // Assume split driving
  }

  if (costDifference > 0) {
    this.costSavings(costDifference);
  }

  if (this.calories() !== 0) {
    this.weightLost(parseInt(convert.caloriesToPounds(this.calories()) * m, 10));
  }

  if (timeSavings > 60) {
    this.timeSavings(parseInt(timeSavings / 60 / 60, 10));
  }

  if (emissions > 0) {
    this.emissionsDifference(parseInt(emissions, 10));
  }
};

/**
 * Direct car?
 */

Route.prototype.directCar = function() {
  return this.modes().length === 1 && this.hasCar();
};

/**
 * Is this a direct bike or walk journey?
 */

Route.prototype.directBikeOrWalk = function() {
  return !this.hasTransit() && !this.hasCar();
};

/**
 * Average trip length in minutes
 */

Route.prototype.average = function() {
  if (this.hasTransit() || this.modes().indexOf('car') === -1) {
    return Math.round(this.time() / 60);
  } else {
    return Math.round(this.time() / 60 * 1.35);
  }
};

/**
 * Freeflow
 */

Route.prototype.freeflowTime = function() {
  if (this.hasTransit() || this.modes().indexOf('car') === -1) {
    return false;
  } else {
    return Math.round(this.time() / 60);
  }
};

/**
 * Time in transit
 */

Route.prototype.timeInTransit = function() {
  if (!this.hasTransit()) {
    return 0;
  } else {
    return this.transit().reduce(function(m, t) {
      return m + t.waitStats.avg + t.rideStats.avg;
    }, 0) / 60;
  }
};

/**
 * Shorthand helpers
 */

Route.prototype.hasCost = function() {
  return this.cost() > 0;
};

Route.prototype.hasCar = function() {
  return this.modes().indexOf('car') !== -1;
};

Route.prototype.hasTransit = function() {
  return this.transit().length > 0;
};

Route.prototype.hasBiking = function() {
  return this.modes().indexOf('bicycle') !== -1;
};

Route.prototype.hasWalking = function() {
  return this.modes().indexOf('walk') !== -1;
};

/**
 * Days
 */

Route.prototype.tripsPerYear = function() {
  return session.plan().tripsPerYear();
};

/**
 * Trip multiplier
 */

Route.prototype.tripm = function() {
  return session.plan().tripsPerYear();
};

/**
 * Cost
 */

Route.prototype.calculatedCost = function() {
  if (this.cost() === 0) {
    return false;
  }

  var cost = 0;
  if (this.transitCost()) {
    cost += this.transitCost();
  }
  if (this.modes().indexOf('car') !== -1) {
    cost += this.vmtRate() * this.driveDistances();
    cost += this.carParkingCost();
  }

  var total = cost * this.tripm();
  if (total > 100) {
    return parseInt(total, 10);
  } else {
    return total.toFixed(2);
  }
};

/**
 * Transit Cost
 */

Route.prototype.transitCosts = function() {
  if (!this.transitCost()) {
    return false;
  } else {
    return this.transitCost().toFixed(2);
  }
};

/**
 * Total Calories
 */

Route.prototype.totalCalories = function() {
  if (this.walkDistances() === 0 && this.bikeDistances() === 0) return 0;

  var cals = walkingCaloriesBurned(this.walkSpeed(), this.weight(), this.walkDistance() / this.walkSpeed() / 60 / 60);
  if (this.modes().indexOf('bicycle') !== -1) {
    cals += bikingCaloriesBurned(this.bikeSpeed(), this.weight(), this.bikeDistance() / this.bikeSpeed() / 60 / 60);
  }

  return cals;
};

/**
 * Frequency
 */

Route.prototype.frequency = function() {
  var trips = this.trips();
  if (!trips) {
    return false;
  }

  var plan = session.plan();
  var start = plan.start_time();
  var end = plan.end_time();

  return Math.round(60 / (trips / (end - start)));
};

/**
 * Walk/Bike distances rounded
 */

Route.prototype.driveDistances = function() {
  return this.distances('car', 'driveDistance');
};

Route.prototype.bikeDistances = function() {
  return this.distances('bicycle', 'bikeDistance');
};

Route.prototype.walkDistances = function() {
  return this.distances('walk', 'walkDistance');
};

Route.prototype.distances = function(mode, val) {
  if (this.modes().indexOf(mode) === -1) {
    return false;
  } else {
    return convert.metersToMiles(this[val]());
  }
};

/**
 * Walk/bike speed in MPH
 */

Route.prototype.bikeSpeedMph = function() {
  return toFixed(this.bikeSpeed() * MPS_TO_MPH, 1);
};

Route.prototype.walkSpeedMph = function() {
  return toFixed(this.walkSpeed() * MPS_TO_MPH, 1);
};

/**
 * Walk/bike time in minutes
 */

Route.prototype.bikeTime = function() {
  return timeFromSpeedAndDistance(this.bikeSpeed(), this.bikeDistance());
};

Route.prototype.walkTime = function() {
  return timeFromSpeedAndDistance(this.walkSpeed(), this.walkDistance());
};

function timeFromSpeedAndDistance(s, d) {
  var t = d / s;
  if (t < 60) {
    return '< 1';
  } else {
    return parseInt(t / 60, 10);
  }
}

/**
 * Retrieve from scorer
 */

Route.prototype.bikeSpeed = function() {
  return session.plan().scorer().rates.bikeSpeed;
};

Route.prototype.walkSpeed = function() {
  return session.plan().scorer().rates.walkSpeed;
};

Route.prototype.vmtRate = function() {
  return session.plan().scorer().rates.mileageRate;
};

Route.prototype.weight = function() {
  return session.plan().scorer().rates.weight;
};

Route.prototype.carParkingCost = function() {
  return session.plan().scorer().rates.carParkingCost;
};

/**
 * Construct a simple mode-based descriptor (e.g. "Drive to Transit")
 */

Route.prototype.modeDescriptor = function() {
  var modeStr;

  if (this.modes().indexOf('bicycle_rent') !== -1) {
    modeStr = 'bikeshare';
  } else if (this.bikeDistance() > 0) {
    modeStr = 'bike';
  } else if (this.driveDistance() > 0) {
    modeStr = 'drive';
  } else {
    modeStr = 'walk';
  }

  if (this.hasTransit()) {
    modeStr += ' to transit';
  } else if (this.driveDistance() > 0) {
    modeStr = 'rideshare';
  }

  return modeStr;
};

/**
 * Walking Calories
 *
 * CB = [0.0215 x KPH3 - 0.1765 x KPH2 + 0.8710 x KPH + 1.4577] x WKG x T
 * http://www.shapesense.com/fitness-exercise/calculators/walking-calorie-burn-calculator.aspx
 */

function walkingCaloriesBurned(mps, wkg, hours) {
  var kph = mps / 1000 * 60 * 60;
  var kph2 = kph * kph;
  var kph3 = kph2 * kph;
  return (0.0215 * kph3 - 0.1765 * kph2 + 0.8710 * kph) * wkg * hours;
}

/**
 * Biking Calories
 *
 * http://en.wikipedia.org/wiki/Bicycle_performance
 */

var GRADE = 1;
var GRAVITY = 9.8;
var K1 = 0.0053; // frictional losses
var K2 = 0.185; // aerodynamic drag
var WATTS_TO_CALS_PER_SECOND = 0.2388;

function bikingCaloriesBurnedOld(mps, wkg, hours) {
  var mps3 = Math.pow(mps, 3);
  var seconds = hours * 60 * 60;
  var watts = GRAVITY * wkg * mps * (K1 + GRADE) + K2 * mps3;
  return watts * WATTS_TO_CALS_PER_SECOND * seconds;
}

function bikingCaloriesBurned(mps, wkg, hours) {
  return 8 * wkg * hours;
}

function toFixed(n, f) {
  var m = Math.pow(10, f);
  return ((n * m) | 0) / m;
}

/**
 * Tags
 */

Route.prototype.tags = function(plan) {
  var tags = [];

  // add the access mode tags
  each(this.get('access'), function(accessLeg) {
    tags.push(accessLeg.mode);
  });

  // add tags for each transit leg
  each(this.transit(), function(transitLeg) {
    tags.push(transitLeg.mode); // add the transit mode tag
    if (transitLeg.routes.length > 0) { //add the agency tag
      tags.push(transitLeg.routes[0].id.split(':')[0]);
    }
  });

  // add a generic 'transit' tag
  if (this.hasTransit()) tags.push('transit');

  // add tags for the from/to locations
  if (plan) {
    var from = locationToTags(plan.from());
    var to = locationToTags(plan.to());
    tags = tags.concat(from).concat(to);
  }

  tags = tags.map(function(tag) {
    return tag.toLowerCase().trim();
  });
  return tags;
};

function locationToTags(location) {
  // strip off the zip code, if present
  var endsWithZip = /\d{5}$/;
  if (endsWithZip.test(location)) {
    location = location.substring(0, location.length - 5);
  }
  return location.split(',').slice(1);
}
<|endoftext|>


<commit_before>var Thunder = new function() {
    this.channels = [];
    this.handlers = [];

    this.reconnect_delays = [1000, 2500, 5000, 10000, 30000, 60000];

    this.options = {
        // verbose?
        log: false
    };

    this.connect = function(server, apikey, channels, options) {
        this.server = "http://" + server + "/connect";
        this.apikey = apikey;
        this.channels = channels;
        this.reconnect_tries = 0;

        // merge options
        for (var attr in options) {
            this.options[attr] = options[attr];
        }

        this.user = this.options.user;
        this.makeConnection();

        var that = this;
    };

    this.listen = function(handler) {
        this.log("New handler has been registered.");
        this.handlers.push(handler);
    };

    this.makeConnection = function() {
        var that = this;

        // make a connection
        this.socket = new SockJS(this.server, undefined,
            {'debug': this.options.log});

        this.socket.onopen = function() {
            that.log("Connection has been estabilished.");

            // reset retries counter
            that.reconnect_tries = 0;

            // connect and subscribe to channels
            that.socket.send("CONNECT " + that.user + ":" + that.apikey);

            if (that.channels.length)
                that.socket.send("SUBSCRIBE " + that.channels.join(":"));
        }

        this.socket.onmessage = function(e) {
            that.log("Message has been received", e.data);

            try {
                // try to parse the message as json
                var json_data = JSON.parse(e.data);
                e.data = json_data;
            }
            catch(e) {
                // not json, leave it as is
            }

            for (var i = 0; i < that.handlers.length; i++) {
                that.handlers[i](e.data);
            }
        }

        this.socket.onclose = function(e) {
            that.log("Connection has been lost.");

            if (e.code == 9000 || e.code == 9001 || e.code == 9002) {
                // received "key not good" close message
                that.log("Reconnect supressed because of:", e);
                return;
            }

            var delay = that.reconnect_delays[that.reconnect_tries]
                || that.reconnect_delays[that.reconnect_delays.length - 1];

            that.log("Reconnecting in", delay, "ms...");
            that.reconnect_tries++;

            setTimeout(function() {
                that.makeConnection();
            }, delay);
        }
    };

    this.log = function(msg) {
        if (this.options.log
                && "console" in window && "log" in window.console) {

            if (arguments.length == 1) {
                console.log(arguments[0]);
            }
            else {
                console.log.apply(console,
                    Array.prototype.slice.call(arguments));
            }
        }
    };
}<commit_msg>Internet Explorer 9′s problematic console object<commit_after>var isMSIE = /*@cc_on!@*/0;
var Thunder = new function() {
    this.channels = [];
    this.handlers = [];

    this.reconnect_delays = [1000, 2500, 5000, 10000, 30000, 60000];

    this.options = {
        // verbose?
        log: false
    };

    this.connect = function(server, apikey, channels, options) {
        this.server = "http://" + server + "/connect";
        this.apikey = apikey;
        this.channels = channels;
        this.reconnect_tries = 0;

        // merge options
        for (var attr in options) {
            this.options[attr] = options[attr];
        }

        this.user = this.options.user;
        this.makeConnection();

        var that = this;
    };

    this.listen = function(handler) {
        this.log("New handler has been registered.");
        this.handlers.push(handler);
    };

    this.makeConnection = function() {
        var that = this;

        // make a connection
        this.socket = new SockJS(this.server, undefined,
            {'debug': this.options.log});

        this.socket.onopen = function() {
            that.log("Connection has been estabilished.");

            // reset retries counter
            that.reconnect_tries = 0;

            // connect and subscribe to channels
            that.socket.send("CONNECT " + that.user + ":" + that.apikey);

            if (that.channels.length)
                that.socket.send("SUBSCRIBE " + that.channels.join(":"));
        }

        this.socket.onmessage = function(e) {
            that.log("Message has been received", e.data);

            try {
                // try to parse the message as json
                var json_data = JSON.parse(e.data);
                e.data = json_data;
            }
            catch(e) {
                // not json, leave it as is
            }

            for (var i = 0; i < that.handlers.length; i++) {
                that.handlers[i](e.data);
            }
        }

        this.socket.onclose = function(e) {
            that.log("Connection has been lost.");

            if (e.code == 9000 || e.code == 9001 || e.code == 9002) {
                // received "key not good" close message
                that.log("Reconnect supressed because of:", e);
                return;
            }

            var delay = that.reconnect_delays[that.reconnect_tries]
                || that.reconnect_delays[that.reconnect_delays.length - 1];

            that.log("Reconnecting in", delay, "ms...");
            that.reconnect_tries++;

            setTimeout(function() {
                that.makeConnection();
            }, delay);
        }
    };

    this.log = function(msg) {
        if (this.options.log
                && "console" in window && "log" in window.console) {

            if (arguments.length == 1) {
                console.log(arguments[0]);
            }
            else {
                if (isMSIE) {
                    var log = Function.prototype.bind.call(console.log, console);
                    log.apply(console, Array.prototype.slice.call(arguments));
                } else {
                    console.log.apply(console, Array.prototype.slice.call(arguments));
                }
            }
        }
    };
}<|endoftext|>


<commit_before>/* Takes an AST and converts all of the functions into closures.
 * A closure is a triple of:
 *  a mapping from names to expressions
 *  a mapping from names to type expressions
 *  a function (which includes the formal parameters and the body)
 * The closure has the property that all of the free variables of the function
 * are in the environment, or an exception is raised because the variable is not bound
 * in the current environment.
 * A free variable is simply those that are not in the list of formal parameters.
 * We start with the global environment and traverse the AST. Every time a new function is entered
 * the current environment gets $.extended with the formal parameters of the function.
 * When a let is encountered the current environment also gets $.extended.
 * The algorithm continues for any further function definitions in that branch
 * otherwise it just stops for that particular branch and continues with the rest of the AST
 *
 * Therefore in order to call a closure one must first extract the actual function and then
 * call the function with the environment associated with it.
 * For the purposes of type checking it does not matter how the function gets called, the environment
 * is only used for looking up the types of names. Formal parameters are given type variables.
 */

var rep = require("./representation.js");
var env = require("./environments.js");
var errors = require("./errors.js");
var parser = require("./parse.js");
var pprint = require("./pprint.js");
var $ = require("./tools.js");

var notEmpty = $.compose($.not, $.eq([]));

function fvs_helper(stx) {
  switch (stx.exprType) {
    case "Integer":
      return [];
    case "Float":
      return [];
    case "String":
      return [];
    case "Function":
      return [];
    case "Nil":
      return [];
    case "Bool":
      return [];
    case "Let":
      return [];
    case "Unary":
      return $.flatten([stx.op.ident, fvs_helper(stx.val)]);
    case "Definition":
      return $.flatten(fvs_helper(stx.val));
    case "Application":
      var vs = $.flatten(fvs_helper(stx.p));
      var f_fvs = $.flatten(fvs_helper(stx.func));
      return $.flatten([vs, f_fvs]);
    case "If":
      if (stx.elseexp) {
        var cond_fvs = fvs_helper(stx.condition);
        var then_fvs = fvs_helper(stx.thenexp);
        var else_fvs = fvs_helper(stx.elseexp);
        return $.flatten([cond_fvs, then_fvs, else_fvs]);
      }
      else {
        return $.flatten([fvs_helper(stx.condition), fvs_helper(stx.thenexp)]);
      }
      break;
    case "Name":
      return stx.ident;
  }
}

function fvs(stx) {
  if (stx.exprType !== "Function" &&
      stx.exprType !== "Let") {
    throw errors.JInternalError(
           ["Tried to calculate the free variables of",
           "something that was not a function or let.\n",
           "That something was a: " + stx.exprType +"\n"].reduce(
                function (a,b) {
                  return a+" "+b;
                }, ""));
  }
  var variables, free_variables;

  switch (stx.exprType) {
    case "Let":
      var bound_vars = stx.pairs.map(
         function (stx) {
           return stx.ident.ident;
         });
      var let_fvs = stx.pairs.map(fvs_helper);
      var body_fvs = fvs_helper(stx.body);
      variables = $.flatten(let_fvs);
      $.extend(variables, $.flatten(body_fvs));
      free_variables = $.difference($.unique($.flatten(variables)), bound_vars);
      break;
    case "Function":

  }
}

//var ast = parser.parse("let { c = trtr a = let {tttt = (rtertret^yyyy) } let { dfsdf = let { asdsd = 3434 } gdfgdfg } (45+(asdddy*uyuy))  q = ((lambda x y -> (x+y)) 4 ui) } (^ wat (a+(ar*b*c^twerp+\"sdfdsfsdfsdfsdf\")*rt))")[0];
//var ast = parser.parse("let { a = let { b = let {dsdfgf = sddd } fdgfg } gggggg } t")[0];
//console.log(pprint.pprint(ast));
//var ast = parser.parse("let { a = 12 b = (a + t) } (a + b * d)")[0];
//console.log(fvs(ast));
//var ast = parser.parse("((lambda a b c -> (+ a b c)) 2 3.0 4)");
var ast = parser.parse("def (f a b c) 12")[0];
//console.log(JSON.stringify(ast, null, 4));
console.log(pprint.pprint(ast));
<commit_msg>free variable calculation and naive closure creation works<commit_after>/* Takes an AST and converts all of the functions into closures.
 * A closure is a triple of:
 *  a mapping from names to expressions
 *  a mapping from names to type expressions
 *  a function (which includes the formal parameters and the body)
 * The closure has the property that all of the free variables of the function
 * are in the environment, or an exception is raised because the variable is not bound
 * in the current environment.
 * A free variable is simply those that are not in the list of formal parameters.
 * We start with the global environment and traverse the AST. Every time a new function is entered
 * the current environment gets $.extended with the formal parameters of the function.
 * When a let is encountered the current environment also gets $.extended.
 * The algorithm continues for any further function definitions in that branch
 * otherwise it just stops for that particular branch and continues with the rest of the AST
 *
 * Therefore in order to call a closure one must first extract the actual function and then
 * call the function with the environment associated with it.
 * For the purposes of type checking it does not matter how the function gets called, the environment
 * is only used for looking up the types of names. Formal parameters are given type variables.
 */

var rep = require("./representation.js");
var env = require("./environments.js");
var errors = require("./errors.js");
var parser = require("./parse.js");
var pprint = require("./pprint.js");
var $ = require("./tools.js");

var notEmpty = $.compose($.not, $.eq([]));

function fvs_helper(stx) {
  switch (stx.exprType) {
    case "Integer":
      return [];
    case "Float":
      return [];
    case "String":
      return [];
    case "Function":
      return [];
    case "Nil":
      return [];
    case "Bool":
      return [];
    case "Let":
      return [];
    case "Unary":
      return $.flatten([stx.op.ident, fvs_helper(stx.val)]);
    case "Definition":
      return $.flatten(fvs_helper(stx.val));
    case "Application":
      var vs = $.flatten(fvs_helper(stx.p));
      var f_fvs = $.flatten(fvs_helper(stx.func));
      return $.flatten([vs, f_fvs]);
    case "If":
      if (stx.elseexp) {
        var cond_fvs = fvs_helper(stx.condition);
        var then_fvs = fvs_helper(stx.thenexp);
        var else_fvs = fvs_helper(stx.elseexp);
        return $.flatten([cond_fvs, then_fvs, else_fvs]);
      }
      else {
        return $.flatten([fvs_helper(stx.condition), fvs_helper(stx.thenexp)]);
      }
      break;
    case "Name":
      return [stx.ident];
  }
}

function fvs(stx) {
  if (stx.exprType !== "Function" &&
      stx.exprType !== "Let") {
    throw errors.JInternalError(
           ["Tried to calculate the free variables of",
           "something that was not a function or let.\n",
           "That something was a: " + stx.exprType +"\n"].reduce(
                function (a,b) {
                  return a+" "+b;
                }, ""));
  }
  var variables, free_variables, bound_vars;

  switch (stx.exprType) {
    case "Let":
      bound_vars = stx.pairs.map(
         function (stx) {
           return stx.ident.ident;
         });
      var let_fvs = stx.pairs.map(fvs_helper);
      var body_fvs = fvs_helper(stx.body);
      variables = $.flatten(let_fvs);
      $.extend(variables, $.flatten(body_fvs));
      break;
    case "Function":
      bound_vars = [stx.p.ident,];
      variables = fvs_helper(stx.body);
  }
  free_variables = $.difference($.unique(variables, bound_vars));
  return new rep.Closure(bound_vars, free_variables, stx, []);
}

//var ast = parser.parse("let { c = trtr a = let {tttt = (rtertret^yyyy) } let { dfsdf = let { asdsd = 3434 } gdfgdfg } (45+(asdddy*uyuy))  q = ((lambda x y -> (x+y)) 4 ui) } (^ wat (a+(ar*b*c^twerp+\"sdfdsfsdfsdfsdf\")*rtcccc))")[0];
//var ast = parser.parse("let { a = let { b = let {dsdfgf = sddd } fdgfg } gggggg } t")[0];
//console.log(pprint.pprint(ast));
//var ast = parser.parse("let { a = 12 b = (a + t) } (a + b * d)")[0];
//console.log(fvs(ast));
//var ast = parser.parse("((lambda a b c -> (+ a b c)) 2 3.0 4)");
//var ast = parser.parse("def (f a b c) 12")[0];
//console.log(JSON.stringify(ast, null, 4));
//console.log(pprint.pprint(ast));
var ast = parser.parse("(lambda a -> (pi*e+c-a))")[0];
console.log(fvs(ast));
<|endoftext|>


<commit_before>module.exports = function(grunt) {
  grunt.registerMultiTask('browser', "Export a module to the window", function() {
    var opts = this.options();
    this.files.forEach(function(f) {
      var output = ["(function(globals) {"];

      output.push.apply(output, f.src.map(grunt.file.read));

      output.push(grunt.template.process(
        'window.<%= namespace %> = requireModule("<%= barename %>");', {
        data: {
          namespace: opts.namespace,
          barename: opts.barename
        }
      }));
      output.push('})(window);');

      grunt.file.write(f.dest, grunt.template.process(output.join("\n")));
    });
  });
};
<commit_msg>define Ember shim for ember-oauth2 dependency<commit_after>module.exports = function(grunt) {

  grunt.registerMultiTask('browser', "Export a module to the window", function() {
    var opts = this.options();
    this.files.forEach(function(f) {
      var output = ["(function(globals) {"];

      output.push.apply(output, f.src.map(grunt.file.read));

      output.push('\n/*global define, Ember */\ndefine("ember", [], function() {\n\t"use strict";\n\treturn { "default": Ember };\n });\n');

      output.push(grunt.template.process(
        'window.<%= namespace %> = requireModule("<%= barename %>")["default"];', {
        data: {
          namespace: opts.namespace,
          barename: opts.barename
        }
      }));
      output.push('})(window);');

      grunt.file.write(f.dest, grunt.template.process(output.join("\n")));
    });
  });
};
<|endoftext|>


<commit_before>//save requests to files
var mongo = require('./mongoFunctions.js'),
	assert = require('assert'),
	request= require('request'),
	jsonFromHtml = require('./getJsonFromHtml.js'),
	userMod = require('./getSubstitution.js'),
	querystring = require('querystring'),
	messenger = require('./messengerBot.js'),
	setTime = require('./setTime.js'),
	callFunc = require('./postCallFunctions.js');

/*
	module to comunicate with ZSO11 server
*/

var time = new setTime();

var getSomeSubstitution = function(date,callback){
	time.tommorowIs();
	var tomorrow = time.displayTime();
	var day = '';
	if (date == tomorrow){
		day = 'tomorrow';
	} else {
		time.todayIs();
		var today = time.displayTime();
		if (date == today){
			day = 'today';
		} else {
			time.theDayAfterTomorrowIs();
			var tdat = time.displayTime();
			if (date == tdat){
				day = 'TDAT'; //The Day After Tomorrow
			}
		}
	}
	getData(date,function(data){
		convertToSubstitutions(data,function(convertedData){
			classListFromDate(convertedData,function(res){
				teachersList(convertedData, function(teachers){
					var dataToSave={};
					dataToSave['substitution']=convertedData;
					dataToSave['userList']=res;
					dataToSave['teachersList'] = teachers;
					console.log('before saving'+ dataToSave['userList']);
					saveSubstitutions(date,dataToSave,function(){
						mongo.findById(date,'substitutions',function(err,x){
							console.log('save substitution '+  x.userList,x.date,x.teachersList);
							setImmediate(function() {
								callback(convertedData);
								messenger.notification(day, date, function(res){
									console.log(res);
								});
							});
						})
					})
					mongo.findById('all', 'teachers', function(erro, obj){
						var beforeTe = obj.teachers;
						var newTe = beforeTe.concat(teachers);
						newTe = uniq(newTe);
						if(newTe.prototype.equals(beforeTe)){
							mongo.modifyById('all', 'teachers', {'teachers': teachers}, function(){
								console.log('Teachers list updated');
							})
						}
					})
				})
			})
		})
	})
}

function teachersList(convertedData, callback){
	var teachers = [];
	if(convertedData != 'no substitutions'){
		for(var n = 0; n < convertedData.length; n++){
			var oneSub = convertedData[n];
			if(oneSub.teachers){
				teachers = teachers.concat(oneSub.teachers);
			}
			if(oneSub.changes && oneSub.changes.teachers){
				teachers = teachers.concat(oneSub.changes.teachers);
			}
		}
		teachers = uniq(teachers);
		setImmediate(function(){
			callback(teachers);
		});
	} else {
		setImmediate(function(){
			callback([]);
		});
	}
}

//function sorting array and removing duplicates
function uniq(a) {
	return a.sort().filter(function(item, pos, ary){
		return !pos || item != ary[pos - 1];
	})
}

function classListFromDate(convertedData,callback){
	var classList=[];
	if (convertedData =='no substitutions'){
		setImmediate(function(){
			callback({});
		});
	}
	else{
		var changes = convertedData;
		for(var i=0;i<changes.length;i++){
			var oneChange = changes[i];
			var newClass = oneChange['classes'];
			for(var j =0;j<newClass.length;j++){
				if(classList.indexOf(newClass[j])==-1){
					classList[classList.length]=newClass[j];
				}
			}
		}
		setImmediate(function(){
			callback(classList);
		});
	}
}

var getData = function(date,callback){
	downloadData(date,function(err,body){
		if(err){
			getCookie(function(){
				downloadData(date,function(err1,newBody){
					setImmediate(function() {
						callback(newBody);
					});
				})
			});
		}
		else{
			setImmediate(function() {
				callback(body);
			});
		}
	});
}

function getParams(callback){
	var url='http://zso11.edupage.org/substitution/?';
	request(url,function(err,res,body){
		if(!err && res.statusCode ==200){
			getGPIDandGSH(body,function(params){
				var cookie=res.headers['set-cookie'];
				var data = {
					'gpid':params[0],
					'gsh':params[1],
					'cookie':cookie
				}
				saveToCollection(['testCollection',data],function(){
					console.log('data added: '+data);
				})
			});
			//save somewhere
		}
		setImmediate(function() {
			callback();
		});
	});
}

function downloadData(date,callback){
	var url1='http://zso11.edupage.org/gcall';
	mongo.findById('params','testCollection',function(err1,params){
		console.log('params',params);
		var cookie=params['cookie'];
		var form = {
			gpid:params['gpid'],
			gsh:params['gsh'],
			action:"switch",
			date:date,
			_LJSL:"2,1"
		};
		var formData = querystring.stringify(form);
		var contentLength = formData.length;
		request({
			headers: {
				'Content-Length': contentLength,
				'Content-Type': 'application/x-www-form-urlencoded',
				'Cookie' : cookie
			},
			uri: url1,
			body: formData,
			method: 'POST'
		}, function (err, res, body) {
			assert.equal(null,err);
			setImmediate(function() {
				callback(body.length<100,body);
			});
		});
	});
}

var getCookie=function(callback){
	var url='http://zso11.edupage.org/substitution/?';
	request(url,function(err,res,body){
		if(!err && res.statusCode ==200){
			getGPIDandGSH(body,function(params){
				var cookie=res.headers['set-cookie'];
				var data = {
					'gpid':params[0],
					'gsh':params[1],
					'cookie':cookie
				}
				console.log(JSON.stringify(data));
				mongo.modifyById('params','testCollection',data,function(){
					console.log('cookies saved: '+data);
					setImmediate(function() {
						callback(data);
					});
				})
			});
			//save somewhere
		}
	});
}

function checkIfAnySubstitutions(callback){
	var res;
	getData(date,function(body){
		if(body.indexOf('column') != -1){
			res=true;
			console.log('i see something');
		}else{
			res=false;
			console.log('nothing')
		}
		setImmediate(function() {
			callback(res);
		});
	})
}

function saveSubstitutions(date,data,callback){
	var dataToSave={};
		dataToSave['substitution']=data.substitution;
		dataToSave['userList']=data.userList;
		dataToSave['date']=date;
		dataToSave['teachersList'] = data.teachersList;
	mongo.modifyById(date,'substitutions',dataToSave,function(){
		setImmediate(function() {
			callback(); //callback not necessary
		});
		//ok;
	})
}

function convertToSubstitutions(data,callback){
	var substitution2 =new jsonFromHtml();
	var user2= new userMod();
	var allChanges;
	substitution2.fileString=data;
	var err=substitution2.testIfCorrectFile();
	if(err){
		substitution2.getJsonObj();
		user2.keyArray=JSON.parse(substitution2.keyObj);
		user2.dataArray=JSON.parse(substitution2.dataObj);
		allChanges=user2.changes();
	}
	else{
		allChanges='no substitutions';
	}
	setImmediate(function() {
		callback(allChanges); //respond with converted changes
	});
}

function getGPIDandGSH (data,callback){ //take params to send request
	var a=data.indexOf('gsh');
	var gpidIndex;
	var gshIndex;
	for (var i=0; i<11;i++){
		if (data.charAt(a-i)=='='){
			gpidIndex=a-i;
			break;
		}
	}
	for (var i=0; i<20;i++){
		if (data.charAt(a+i)=='"'){
			gshIndex=a+i;
			break;
		}
	}
	var gpid=data.slice(gpidIndex+1,a-1);
	var gsh=data.slice(a+4,gshIndex);
	setImmediate(function() {
		callback([gpid,gsh]);
	});
}

/*
	define Array.prototype.equals - campares arrays and returns true if they are the same
	doesn't work with objects as elements of an array (2 same objects always are different)
	source: http://stackoverflow.com/questions/7837456/how-to-compare-arrays-in-javascript/14853974#14853974
*/

// Warn if overriding existing method
if(Array.prototype.equals)
	console.warn("Overriding existing Array.prototype.equals. Possible causes: New API defines the method, there's a framework conflict or you've got double inclusions in your code.");
// attach the .equals method to Array's prototype to call it on any array
Array.prototype.equals = function (array) {
	// if the other array is a falsy value, return
	if (!array)
		return false;

	// compare lengths - can save a lot of time
	if (this.length != array.length)
		return false;

	for (var i = 0, l=this.length; i < l; i++) {
		// Check if we have nested arrays
		if (this[i] instanceof Array && array[i] instanceof Array) {
			// recurse into the nested arrays
			if (!this[i].equals(array[i]))
				return false;
		}
		else if (this[i] != array[i]) {
			// Warning - two different object instances will never be equal: {x:20} != {x:20}
			return false;
		}
	}
	return true;
}
// Hide method from for-in loops
Object.defineProperty(Array.prototype, "equals", {enumerable: false});

exports.subs = getSomeSubstitution;
exports.getData = getData;
exports.downloadData = downloadData;
exports.getCookie = getCookie;
exports.convert = convertToSubstitutions;
exports.save = saveSubstitutions;
exports.getGPIDandGSH = getGPIDandGSH;<commit_msg>Fix bugs made in cc34c20<commit_after>//save requests to files
var mongo = require('./mongoFunctions.js'),
	assert = require('assert'),
	request= require('request'),
	jsonFromHtml = require('./getJsonFromHtml.js'),
	userMod = require('./getSubstitution.js'),
	querystring = require('querystring'),
	messenger = require('./messengerBot.js'),
	setTime = require('./setTime.js'),
	callFunc = require('./postCallFunctions.js');

/*
	module to comunicate with ZSO11 server
*/

var time = new setTime();

var getSomeSubstitution = function(date,callback){
	time.tommorowIs();
	var tomorrow = time.displayTime();
	var day = '';
	if (date == tomorrow){
		day = 'tomorrow';
	} else {
		time.todayIs();
		var today = time.displayTime();
		if (date == today){
			day = 'today';
		} else {
			time.theDayAfterTomorrowIs();
			var tdat = time.displayTime();
			if (date == tdat){
				day = 'TDAT'; //The Day After Tomorrow
			}
		}
	}
	getData(date,function(data){
		convertToSubstitutions(data,function(convertedData){
			classListFromDate(convertedData,function(res){
				teachersList(convertedData, function(teachers){
					var dataToSave={};
					dataToSave['substitution']=convertedData;
					dataToSave['userList']=res;
					dataToSave['teachersList'] = teachers;
					console.log('before saving'+ dataToSave['userList']);
					saveSubstitutions(date,dataToSave,function(){
						mongo.findById(date,'substitutions',function(err,x){
							console.log('save substitution '+  x.userList,x.date,x.teachersList);
							setImmediate(function() {
								callback(convertedData);
								messenger.notification(day, date, function(res){
									console.log(res);
								});
							});
						})
					})
					mongo.findById('all', 'teachers', function(erro, obj){
						var beforeTe = obj.teachers;
						var newTe = beforeTe.concat(teachers);
						newTe = uniq(newTe);
						if(!arrayEqual(newTe, beforeTe)){
							mongo.modifyById('all', 'teachers', {'teachers': teachers}, function(){
								console.log('Teachers list updated');
							})
						}
					})
				})
			})
		})
	})
}

function teachersList(convertedData, callback){
	var teachers = [];
	if(convertedData != 'no substitutions'){
		for(var n = 0; n < convertedData.length; n++){
			var oneSub = convertedData[n];
			if(oneSub.teachers){
				teachers = teachers.concat(oneSub.teachers);
			}
			if(oneSub.changes && oneSub.changes.teachers){
				teachers = teachers.concat(oneSub.changes.teachers);
			}
		}
		teachers = uniq(teachers);
		setImmediate(function(){
			callback(teachers);
		});
	} else {
		setImmediate(function(){
			callback([]);
		});
	}
}

//function sorting array and removing duplicates
function uniq(a) {
	return a.sort().filter(function(item, pos, ary){
		return !pos || item != ary[pos - 1];
	})
}

function classListFromDate(convertedData,callback){
	var classList=[];
	if (convertedData =='no substitutions'){
		setImmediate(function(){
			callback({});
		});
	}
	else{
		var changes = convertedData;
		for(var i=0;i<changes.length;i++){
			var oneChange = changes[i];
			var newClass = oneChange['classes'];
			for(var j =0;j<newClass.length;j++){
				if(classList.indexOf(newClass[j])==-1){
					classList[classList.length]=newClass[j];
				}
			}
		}
		setImmediate(function(){
			callback(classList);
		});
	}
}

var getData = function(date,callback){
	downloadData(date,function(err,body){
		if(err){
			getCookie(function(){
				downloadData(date,function(err1,newBody){
					setImmediate(function() {
						callback(newBody);
					});
				})
			});
		}
		else{
			setImmediate(function() {
				callback(body);
			});
		}
	});
}

function getParams(callback){
	var url='http://zso11.edupage.org/substitution/?';
	request(url,function(err,res,body){
		if(!err && res.statusCode ==200){
			getGPIDandGSH(body,function(params){
				var cookie=res.headers['set-cookie'];
				var data = {
					'gpid':params[0],
					'gsh':params[1],
					'cookie':cookie
				}
				saveToCollection(['testCollection',data],function(){
					console.log('data added: '+data);
				})
			});
			//save somewhere
		}
		setImmediate(function() {
			callback();
		});
	});
}

function downloadData(date,callback){
	var url1='http://zso11.edupage.org/gcall';
	mongo.findById('params','testCollection',function(err1,params){
		console.log('params',params);
		var cookie=params['cookie'];
		var form = {
			gpid:params['gpid'],
			gsh:params['gsh'],
			action:"switch",
			date:date,
			_LJSL:"2,1"
		};
		var formData = querystring.stringify(form);
		var contentLength = formData.length;
		request({
			headers: {
				'Content-Length': contentLength,
				'Content-Type': 'application/x-www-form-urlencoded',
				'Cookie' : cookie
			},
			uri: url1,
			body: formData,
			method: 'POST'
		}, function (err, res, body) {
			assert.equal(null,err);
			setImmediate(function() {
				callback(body.length<100,body);
			});
		});
	});
}

var getCookie=function(callback){
	var url='http://zso11.edupage.org/substitution/?';
	request(url,function(err,res,body){
		if(!err && res.statusCode ==200){
			getGPIDandGSH(body,function(params){
				var cookie=res.headers['set-cookie'];
				var data = {
					'gpid':params[0],
					'gsh':params[1],
					'cookie':cookie
				}
				console.log(JSON.stringify(data));
				mongo.modifyById('params','testCollection',data,function(){
					console.log('cookies saved: '+data);
					setImmediate(function() {
						callback(data);
					});
				})
			});
			//save somewhere
		}
	});
}

function checkIfAnySubstitutions(callback){
	var res;
	getData(date,function(body){
		if(body.indexOf('column') != -1){
			res=true;
			console.log('i see something');
		}else{
			res=false;
			console.log('nothing')
		}
		setImmediate(function() {
			callback(res);
		});
	})
}

function saveSubstitutions(date,data,callback){
	var dataToSave={};
		dataToSave['substitution']=data.substitution;
		dataToSave['userList']=data.userList;
		dataToSave['date']=date;
		dataToSave['teachersList'] = data.teachersList;
	mongo.modifyById(date,'substitutions',dataToSave,function(){
		setImmediate(function() {
			callback(); //callback not necessary
		});
		//ok;
	})
}

function convertToSubstitutions(data,callback){
	var substitution2 =new jsonFromHtml();
	var user2= new userMod();
	var allChanges;
	substitution2.fileString=data;
	var err=substitution2.testIfCorrectFile();
	if(err){
		substitution2.getJsonObj();
		user2.keyArray=JSON.parse(substitution2.keyObj);
		user2.dataArray=JSON.parse(substitution2.dataObj);
		allChanges=user2.changes();
	}
	else{
		allChanges='no substitutions';
	}
	setImmediate(function() {
		callback(allChanges); //respond with converted changes
	});
}

function getGPIDandGSH (data,callback){ //take params to send request
	var a=data.indexOf('gsh');
	var gpidIndex;
	var gshIndex;
	for (var i=0; i<11;i++){
		if (data.charAt(a-i)=='='){
			gpidIndex=a-i;
			break;
		}
	}
	for (var i=0; i<20;i++){
		if (data.charAt(a+i)=='"'){
			gshIndex=a+i;
			break;
		}
	}
	var gpid=data.slice(gpidIndex+1,a-1);
	var gsh=data.slice(a+4,gshIndex);
	setImmediate(function() {
		callback([gpid,gsh]);
	});
}

function arrayEqual(array0, array){
	if (array0.length != array.length){
		return false;
	}
	for (var i = 0; i < array0.length; i++) {
		if (array0[i] != array[i]) {
			return false;
		}
	}
	return true;
}

exports.subs = getSomeSubstitution;
exports.getData = getData;
exports.downloadData = downloadData;
exports.getCookie = getCookie;
exports.convert = convertToSubstitutions;
exports.save = saveSubstitutions;
exports.getGPIDandGSH = getGPIDandGSH;<|endoftext|>