<commit_before>var db = require("../database");
var valid = require("../utilities").isValidChannelName;

var blackHole = function () { };

function dropTable(name, callback) {
    db.query("DROP TABLE `" + name + "`", callback);
}

function createRanksTable(name, callback) {
    db.query("CREATE TABLE `chan_" + name + "_ranks` (" +
                "`name` VARCHAR(20) NOT NULL," +
                "`rank` INT NOT NULL," +
             "PRIMARY KEY (`name`)) " +
             "CHARACTER SET utf8", callback);
}

function createLibraryTable(name, callback) {
    db.query("CREATE TABLE `chan_" + name + "_library` (" +
                "`id` VARCHAR(255) NOT NULL," +
                "`title` VARCHAR(255) NOT NULL," +
                "`seconds` INT NOT NULL," +
                "`type` VARCHAR(2) NOT NULL," +
             "PRIMARY KEY (`id`))" +
             "CHARACTER SET utf8", callback);
}

function createBansTable(name, callback) {
    db.query("CREATE TABLE `chan_" + name + "_bans` (" +
                "`ip` VARCHAR(39) NOT NULL," +
                "`name` VARCHAR(20) NOT NULL," +
                "`bannedby` VARCHAR(20) NOT NULL," +
                "`reason` VARCHAR(255) NOT NULL," +
             "PRIMARY KEY (`ip`, `name`))" +
             "CHARACTER SET utf8", callback);
}

function initTables(name, owner, callback) {
    if (!valid(name)) {
        callback("Invalid channel name", null);
        return;
    }

    createRanksTable(name, function (err) {
        if (err) {
            callback(err, null);
            return;
        }

        // TODO add owner to ranks table

        createLibraryTable(name, function (err) {
            if (err) {
                dropTable("chan_" + name + "_ranks");
                callback(err, null);
                return;
            }

            createBansTable(name, function (err) {
                if (err) {
                    dropTable("chan_" + name + "_ranks");
                    dropTable("chan_" + name + "_library");
                    callback(err, null);
                    return;
                }

                callback(null, true);
            });
        });
    });
}

module.exports = {
    /**
     * Initialize the channels table
     */
    init: function () {
        db.query("CREATE TABLE IF NOT EXISTS `channels` (" +
                    "`id` INT NOT NULL AUTO_INCREMENT," +
                    "`name` VARCHAR(30) NOT NULL," +
                    "`owner` VARCHAR(20) NOT NULL," +
                    "`time` BIGINT NOT NULL," +
                 "PRIMARY KEY (`id`), INDEX(`name`), INDEX(`owner`))" +
                 "CHARACTER SET utf8");
    },

    /**
     * Checks if the given channel name is registered
     */
    isChannelTaken: function (name, callback) {
        if (typeof callback !== "function") {
            return;
        }

        if (!valid(name)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("SELECT name FROM `channels` WHERE name=?",
                     [name],
                     function (err, rows) {
            if (err) {
                callback(err, true);
                return;
            }
            callback(null, rows.length > 0);
        });
    },

    /**
     * Looks up a channel
     */
    lookup: function (name, callback) {
        if (typeof callback !== "function") {
            return;
        }

        if (!valid(name)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("SELECT * FROM `channels` WHERE name=?",
                     [name],
                     function (err, rows) {
            if (err) {
                callback(err, null);
                return;
            }

            if (rows.length === 0) {
                callback("No such channel", null);
            } else {
                callback(null, rows[0]);
            }
        });
    },

    /**
     * Searches for a channel
     */
    search: function (name, callback) {
        if (typeof callback !== "function") {
            return;
        }

        db.query("SELECT * FROM `channels` WHERE name LIKE ?",
                     ["%" + name + "%"],
                     function (err, rows) {
            if (err) {
                callback(err, null);
                return;
            }
            callback(null, rows);
        });
    },

    /**
     * Validates and registers a new channel
     */
    register: function (name, owner, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (typeof name !== "string" || typeof owner !== "string") {
            callback("Name and owner are required for channel registration", null);
            return;
        }

        if (!valid(name)) {
            callback("Invalid channel name.  Channel names may consist of 1-30 " +
                     "characters a-z, A-Z, 0-9, -, and _", null);
            return;
        }

        module.exports.isChannelTaken(name, function (err, taken) {
            if (err) {
                callback(err, null);
                return;
            }

            if (taken) {
                callback("Channel name " + name + " is already taken", null);
                return;
            }

            db.query("INSERT INTO `channels` " +
                         "(`name`, `owner`, `time`) VALUES (?, ?, ?)",
                         [name, owner, Date.now()],
                         function (err, res) {
                if (err) {
                    callback(err, null);
                    return;
                }

                initTables(name, owner, function (err, res) {
                    if (err) {
                        db.query("DELETE FROM `channels` WHERE name=?", [name]);
                        callback(err, null);
                        return;
                    }
                    callback(null, {
                        name: name
                    });
                });
            });
        });
    },

    /**
     * Unregisters a channel
     */
    drop: function (name, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(name)) {
            callback("Invalid channel name", null);
            return;
        }

        dropTable("chan_" + name + "_ranks", function (err) {
            dropTable("chan_" + name + "_bans", function (e2) {
                if (err && e2) {
                    err += "\n" + e2;
                } else if (e2) {
                    err = e2;
                }

                dropTable("chan_" + name + "_library", function (e3) {
                    if (err && e3) {
                        err += "\n" + e3;
                    } else if (e3) {
                        err = e3;
                    }

                    db.query("DELETE FROM `channels` WHERE name=?", [name],
                    function (e4) {
                        if (err && e4) {
                            err += "\n" + e4;
                        } else if (e4) {
                            err = e4;
                        }

                        callback(err, !Boolean(err));
                    });
                });
            });
        });
    },

    /**
     * Looks up channels registered by a given user
     */
    listUserChannels: function (owner, callback) {
        if (typeof callback !== "function") {
            return;
        }

        db.query("SELECT * FROM `channels` WHERE owner=?", [owner],
        function (err, res) {
            if (err) {
                callback(err, []);
                return;
            }

            callback(err, res);
        });
    },

    /**
     * Loads the channel from the database
     */
    load: function (chan, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan.name)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("SELECT * FROM `channels` WHERE name=?", chan.name, function (err, res) {
            if (err) {
                callback(err, null);
                return;
            }

            if (res.length === 0) {
                callback("Channel is not registered", null);
                return;
            }

            if (chan.dead) {
                callback("Channel is dead", null);
                return;
            }

            // Note that before this line, chan.name might have a different capitalization
            // than the database has stored.  Update accordingly.
            chan.name = res[0].name;
            chan.canonical_name = chan.name.toLowerCase();
            chan.registered = true;

            // Load bans
            db.query("SELECT * FROM `chan_" + chan.name + "_bans`", function (err, rows) {
                if (chan.dead) {
                    callback("Channel is dead", null);
                    return;
                }

                if (err) {
                    callback(err, null);
                    return;
                }

                for (var i = 0; i < rows.length; i++) {
                    var r = rows[i];
                    if (r.ip === "*") {
                        chan.namebans[r.name] = r;
                    } else {
                        chan.ipbans[r.ip] = r;
                    }
                }

                chan.logger.log("*** Loaded channel from database");
                callback(null, true);
            });
        });
    },

    /**
     * Looks up a user's rank
     */
    getRank: function (chan, name, callback) {
        if (typeof callback !== "function") {
            return;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("SELECT name,rank FROM `chan_" + chan + "_ranks` WHERE name=?",
                 [name],
        function (err, rows) {
            if (err) {
                callback(err, 1);
                return;
            }

            if (rows.length === 0) {
                callback(null, 1);
                return;
            }

            callback(null, rows[0].rank);
        });
    },

    /**
     * Looks up multiple users' ranks at once
     */
    getRanks: function (chan, names, callback) {
        if (typeof callback !== "function") {
            return;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        var replace = "(" + names.map(function () { return "?"; }).join(",") + ")";
        db.query("SELECT name,rank FROM `chan_" + chan + "_ranks` WHERE name IN " +
                 replace,
        function (err, rows) {
            if (err) {
                callback(err, []);
                return;
            }

            callback(null, rows);
        });
    },

    /**
     * Query all user ranks at once
     */
    allRanks: function (chan, callback) {
        if (typeof callback !== "function") {
            return;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("SELECT name,rank FROM `chan_" + chan + "_ranks` WHERE 1", callback);
    },

    /**
     * Updates a user's rank
     */
    setRank: function (chan, name, rank, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("INSERT INTO `chan_" + chan + "_ranks` (name, rank) VALUES (?, ?) " +
                 "ON DUPLICATE KEY UPDATE rank=?", [name, rank, rank], callback);
    },

    /**
     * Inserts a new user rank entry without clobbering an existing one
     */
    newRank: function (chan, name, rank, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("INSERT INTO `chan_" + chan + "_ranks` (name, rank) VALUES (?, ?) " +
                 "ON DUPLICATE KEY UPDATE rank=rank", [name, rank], callback);
    },

    /**
     * Removes a user's rank entry
     */
    deleteRank: function (chan, name, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("DELETE FROM `chan_" + chan + "_ranks` WHERE name=?", [name], callback);
    },

    /**
     * Adds a media item to the library
     */
    addToLibrary: function (chan, media, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("INSERT INTO `chan_" + chan + "_library` (id, title, seconds, type) " +
                 "VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE id=id",
                 [media.id, media.title, media.seconds, media.type], callback);
    },

    /**
     * Retrieves a media item from the library by id
     */
    getLibraryItem: function (chan, id, callback) {
        if (typeof callback !== "function") {
            return;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("SELECT * FROM `chan_" + chan + "_library` WHERE id=?", [id],
        function (err, rows) {
            if (err) {
                callback(err, null);
                return;
            }

            if (rows.length === 0) {
                callback("Item not in library", null);
            } else {
                callback(null, rows[0]);
            }
        });
    },

    /**
     * Search the library by title
     */
    searchLibrary: function (chan, search, callback) {
        if (typeof callback !== "function") {
            return;
        }

        db.query("SELECT * FROM `chan_" + chan + "_library` WHERE title LIKE ?",
                 ["%" + search + "%"], callback);
    },

    /**
     * Deletes a media item from the library
     */
    deleteFromLibrary: function (chan, id, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("DELETE FROM `chan_" + chan + "_library` WHERE id=?", [id], callback);
    },

    /**
     * Add a ban to the banlist
     */
    ban: function (chan, ip, name, note, bannedby, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("INSERT INTO `chan_" + chan + "_bans` (ip, name, reason, bannedby) " +
                 "VALUES (?, ?, ?, ?)", [ip, name, reason, bannedby], callback);
    },

    /**
     * Removes a ban from the banlist
     */
    unbanName: function (chan, name, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("DELETE FROM `chan_" + chan + "_bans` WHERE ip='*' AND name=?",
                 [name], callback);
    },

    /**
     * Removes a ban from the banlist
     */
    unbanIP: function (chan, ip, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("DELETE FROM `chan_" + chan + "_bans` WHERE ip=?",
                 [ip], callback);
    }
};
<commit_msg>Fix channel registration rank from /account/channels<commit_after>var db = require("../database");
var valid = require("../utilities").isValidChannelName;

var blackHole = function () { };

function dropTable(name, callback) {
    db.query("DROP TABLE `" + name + "`", callback);
}

function createRanksTable(name, callback) {
    db.query("CREATE TABLE `chan_" + name + "_ranks` (" +
                "`name` VARCHAR(20) NOT NULL," +
                "`rank` INT NOT NULL," +
             "PRIMARY KEY (`name`)) " +
             "CHARACTER SET utf8", callback);
}

function createLibraryTable(name, callback) {
    db.query("CREATE TABLE `chan_" + name + "_library` (" +
                "`id` VARCHAR(255) NOT NULL," +
                "`title` VARCHAR(255) NOT NULL," +
                "`seconds` INT NOT NULL," +
                "`type` VARCHAR(2) NOT NULL," +
             "PRIMARY KEY (`id`))" +
             "CHARACTER SET utf8", callback);
}

function createBansTable(name, callback) {
    db.query("CREATE TABLE `chan_" + name + "_bans` (" +
                "`ip` VARCHAR(39) NOT NULL," +
                "`name` VARCHAR(20) NOT NULL," +
                "`bannedby` VARCHAR(20) NOT NULL," +
                "`reason` VARCHAR(255) NOT NULL," +
             "PRIMARY KEY (`ip`, `name`))" +
             "CHARACTER SET utf8", callback);
}

function initTables(name, owner, callback) {
    if (!valid(name)) {
        callback("Invalid channel name", null);
        return;
    }

    createRanksTable(name, function (err) {
        if (err) {
            callback(err, null);
            return;
        }

        module.exports.setRank(name, owner, 4, function (err) {
            if (err) {
                dropTable("chan_" + name + "_ranks");
                callback(err, null);
                return;
            }

            createLibraryTable(name, function (err) {
                if (err) {
                    dropTable("chan_" + name + "_ranks");
                    callback(err, null);
                    return;
                }

                createBansTable(name, function (err) {
                    if (err) {
                        dropTable("chan_" + name + "_ranks");
                        dropTable("chan_" + name + "_library");
                        callback(err, null);
                        return;
                    }

                    callback(null, true);
                });
            });
        });
    });
}

module.exports = {
    /**
     * Initialize the channels table
     */
    init: function () {
        db.query("CREATE TABLE IF NOT EXISTS `channels` (" +
                    "`id` INT NOT NULL AUTO_INCREMENT," +
                    "`name` VARCHAR(30) NOT NULL," +
                    "`owner` VARCHAR(20) NOT NULL," +
                    "`time` BIGINT NOT NULL," +
                 "PRIMARY KEY (`id`), INDEX(`name`), INDEX(`owner`))" +
                 "CHARACTER SET utf8");
    },

    /**
     * Checks if the given channel name is registered
     */
    isChannelTaken: function (name, callback) {
        if (typeof callback !== "function") {
            return;
        }

        if (!valid(name)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("SELECT name FROM `channels` WHERE name=?",
                     [name],
                     function (err, rows) {
            if (err) {
                callback(err, true);
                return;
            }
            callback(null, rows.length > 0);
        });
    },

    /**
     * Looks up a channel
     */
    lookup: function (name, callback) {
        if (typeof callback !== "function") {
            return;
        }

        if (!valid(name)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("SELECT * FROM `channels` WHERE name=?",
                     [name],
                     function (err, rows) {
            if (err) {
                callback(err, null);
                return;
            }

            if (rows.length === 0) {
                callback("No such channel", null);
            } else {
                callback(null, rows[0]);
            }
        });
    },

    /**
     * Searches for a channel
     */
    search: function (name, callback) {
        if (typeof callback !== "function") {
            return;
        }

        db.query("SELECT * FROM `channels` WHERE name LIKE ?",
                     ["%" + name + "%"],
                     function (err, rows) {
            if (err) {
                callback(err, null);
                return;
            }
            callback(null, rows);
        });
    },

    /**
     * Validates and registers a new channel
     */
    register: function (name, owner, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (typeof name !== "string" || typeof owner !== "string") {
            callback("Name and owner are required for channel registration", null);
            return;
        }

        if (!valid(name)) {
            callback("Invalid channel name.  Channel names may consist of 1-30 " +
                     "characters a-z, A-Z, 0-9, -, and _", null);
            return;
        }

        module.exports.isChannelTaken(name, function (err, taken) {
            if (err) {
                callback(err, null);
                return;
            }

            if (taken) {
                callback("Channel name " + name + " is already taken", null);
                return;
            }

            db.query("INSERT INTO `channels` " +
                         "(`name`, `owner`, `time`) VALUES (?, ?, ?)",
                         [name, owner, Date.now()],
                         function (err, res) {
                if (err) {
                    callback(err, null);
                    return;
                }

                initTables(name, owner, function (err, res) {
                    if (err) {
                        db.query("DELETE FROM `channels` WHERE name=?", [name]);
                        callback(err, null);
                        return;
                    }
                    callback(null, {
                        name: name
                    });
                });
            });
        });
    },

    /**
     * Unregisters a channel
     */
    drop: function (name, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(name)) {
            callback("Invalid channel name", null);
            return;
        }

        dropTable("chan_" + name + "_ranks", function (err) {
            dropTable("chan_" + name + "_bans", function (e2) {
                if (err && e2) {
                    err += "\n" + e2;
                } else if (e2) {
                    err = e2;
                }

                dropTable("chan_" + name + "_library", function (e3) {
                    if (err && e3) {
                        err += "\n" + e3;
                    } else if (e3) {
                        err = e3;
                    }

                    db.query("DELETE FROM `channels` WHERE name=?", [name],
                    function (e4) {
                        if (err && e4) {
                            err += "\n" + e4;
                        } else if (e4) {
                            err = e4;
                        }

                        callback(err, !Boolean(err));
                    });
                });
            });
        });
    },

    /**
     * Looks up channels registered by a given user
     */
    listUserChannels: function (owner, callback) {
        if (typeof callback !== "function") {
            return;
        }

        db.query("SELECT * FROM `channels` WHERE owner=?", [owner],
        function (err, res) {
            if (err) {
                callback(err, []);
                return;
            }

            callback(err, res);
        });
    },

    /**
     * Loads the channel from the database
     */
    load: function (chan, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan.name)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("SELECT * FROM `channels` WHERE name=?", chan.name, function (err, res) {
            if (err) {
                callback(err, null);
                return;
            }

            if (res.length === 0) {
                callback("Channel is not registered", null);
                return;
            }

            if (chan.dead) {
                callback("Channel is dead", null);
                return;
            }

            // Note that before this line, chan.name might have a different capitalization
            // than the database has stored.  Update accordingly.
            chan.name = res[0].name;
            chan.canonical_name = chan.name.toLowerCase();
            chan.registered = true;

            // Load bans
            db.query("SELECT * FROM `chan_" + chan.name + "_bans`", function (err, rows) {
                if (chan.dead) {
                    callback("Channel is dead", null);
                    return;
                }

                if (err) {
                    callback(err, null);
                    return;
                }

                for (var i = 0; i < rows.length; i++) {
                    var r = rows[i];
                    if (r.ip === "*") {
                        chan.namebans[r.name] = r;
                    } else {
                        chan.ipbans[r.ip] = r;
                    }
                }

                chan.logger.log("*** Loaded channel from database");
                callback(null, true);
            });
        });
    },

    /**
     * Looks up a user's rank
     */
    getRank: function (chan, name, callback) {
        if (typeof callback !== "function") {
            return;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("SELECT name,rank FROM `chan_" + chan + "_ranks` WHERE name=?",
                 [name],
        function (err, rows) {
            if (err) {
                callback(err, 1);
                return;
            }

            if (rows.length === 0) {
                callback(null, 1);
                return;
            }

            callback(null, rows[0].rank);
        });
    },

    /**
     * Looks up multiple users' ranks at once
     */
    getRanks: function (chan, names, callback) {
        if (typeof callback !== "function") {
            return;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        var replace = "(" + names.map(function () { return "?"; }).join(",") + ")";
        db.query("SELECT name,rank FROM `chan_" + chan + "_ranks` WHERE name IN " +
                 replace,
        function (err, rows) {
            if (err) {
                callback(err, []);
                return;
            }

            callback(null, rows);
        });
    },

    /**
     * Query all user ranks at once
     */
    allRanks: function (chan, callback) {
        if (typeof callback !== "function") {
            return;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("SELECT name,rank FROM `chan_" + chan + "_ranks` WHERE 1", callback);
    },

    /**
     * Updates a user's rank
     */
    setRank: function (chan, name, rank, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("INSERT INTO `chan_" + chan + "_ranks` (name, rank) VALUES (?, ?) " +
                 "ON DUPLICATE KEY UPDATE rank=?", [name, rank, rank], callback);
    },

    /**
     * Inserts a new user rank entry without clobbering an existing one
     */
    newRank: function (chan, name, rank, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("INSERT INTO `chan_" + chan + "_ranks` (name, rank) VALUES (?, ?) " +
                 "ON DUPLICATE KEY UPDATE rank=rank", [name, rank], callback);
    },

    /**
     * Removes a user's rank entry
     */
    deleteRank: function (chan, name, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("DELETE FROM `chan_" + chan + "_ranks` WHERE name=?", [name], callback);
    },

    /**
     * Adds a media item to the library
     */
    addToLibrary: function (chan, media, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("INSERT INTO `chan_" + chan + "_library` (id, title, seconds, type) " +
                 "VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE id=id",
                 [media.id, media.title, media.seconds, media.type], callback);
    },

    /**
     * Retrieves a media item from the library by id
     */
    getLibraryItem: function (chan, id, callback) {
        if (typeof callback !== "function") {
            return;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("SELECT * FROM `chan_" + chan + "_library` WHERE id=?", [id],
        function (err, rows) {
            if (err) {
                callback(err, null);
                return;
            }

            if (rows.length === 0) {
                callback("Item not in library", null);
            } else {
                callback(null, rows[0]);
            }
        });
    },

    /**
     * Search the library by title
     */
    searchLibrary: function (chan, search, callback) {
        if (typeof callback !== "function") {
            return;
        }

        db.query("SELECT * FROM `chan_" + chan + "_library` WHERE title LIKE ?",
                 ["%" + search + "%"], callback);
    },

    /**
     * Deletes a media item from the library
     */
    deleteFromLibrary: function (chan, id, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("DELETE FROM `chan_" + chan + "_library` WHERE id=?", [id], callback);
    },

    /**
     * Add a ban to the banlist
     */
    ban: function (chan, ip, name, note, bannedby, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("INSERT INTO `chan_" + chan + "_bans` (ip, name, reason, bannedby) " +
                 "VALUES (?, ?, ?, ?)", [ip, name, reason, bannedby], callback);
    },

    /**
     * Removes a ban from the banlist
     */
    unbanName: function (chan, name, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("DELETE FROM `chan_" + chan + "_bans` WHERE ip='*' AND name=?",
                 [name], callback);
    },

    /**
     * Removes a ban from the banlist
     */
    unbanIP: function (chan, ip, callback) {
        if (typeof callback !== "function") {
            callback = blackHole;
        }

        if (!valid(chan)) {
            callback("Invalid channel name", null);
            return;
        }

        db.query("DELETE FROM `chan_" + chan + "_bans` WHERE ip=?",
                 [ip], callback);
    }
};
<|endoftext|>


<commit_before>const ytdl = require('ytdl-core')
const youtube = new (require('youtube-node'))()

exports.name = 'YouTube'
exports.intents = {
  PlayVideo: (intent, cb) => {
    const title = intent.slots.Title.value
    youtube.setKey(process.env.YOUTUBE_KEY)
    youtube.addParam('type', 'video')
    youtube.search(title, 10, (err, result) => {
      if (!err && result && result.items) {
        const item = result.items[0]
        console.log('preparing video..', item)
        ytdl('https://www.youtube.com/watch?v=' + item.id.videoId,
           { quality: 'highest', filter: format => format.container === 'mp4' },
            (err, info) => {
              console.log('download response:', err, info)
              if (err || !info || !info.formats.length) {
                cb({ text: `Trouble playing YouTube video, please try again later.`, shouldEndSession: true })
              }  else {
                const video = info.formats[0]
                cb({ text: `Playing "${item.snippet.title}" from YouTube`, shouldEndSession: true, mediaUrl: video.url })
              }
            })
      } else {
        cb({ text: 'I could not find any results. Play something else?', shouldEndSession: false })
      }
    })
  },
}
<commit_msg>debugs<commit_after>const ytdl = require('ytdl-core')
const youtube = new (require('youtube-node'))()

exports.name = 'YouTube'
exports.intents = {
  PlayVideo: (intent, cb) => {
    const title = intent.slots.Title.value
    youtube.setKey(process.env.YOUTUBE_KEY)
    youtube.addParam('type', 'video')
    youtube.search(title, 10, (err, result) => {
      if (!err && result && result.items) {
        const item = result.items[0]
        console.log('preparing video..', item, 'https://www.youtube.com/watch?v=' + item.id.videoId')
        ytdl('https://www.youtube.com/watch?v=' + item.id.videoId,
           { quality: 'highest', filter: format => format.container === 'mp4' },
            (err, info) => {
              console.log('download response:', err, info)
              if (err || !info || !info.formats.length) {
                cb({ text: `Trouble playing YouTube video, please try again later.`, shouldEndSession: true })
              }  else {
                const video = info.formats[0]
                cb({ text: `Playing "${item.snippet.title}" from YouTube`, shouldEndSession: true, mediaUrl: video.url })
              }
            })
      } else {
        cb({ text: 'I could not find any results. Play something else?', shouldEndSession: false })
      }
    })
  },
}
<|endoftext|>


<commit_before>/**
 * Created by mox on 23/03/16.
 */

"use strict";

const MODULE_NAME = 'MessageJob';
const OVER_REPETITION_THRSH = 10;
const EventEmitter = require('events');
const util = require('util');
const path = require('path');
const assert = require('assert');
const retry = require('retry');
const async = require('async');
const moment = require('moment');
const funcster = require('funcster');

class MessageJob extends EventEmitter {

    constructor(msg) {
        super();
        if (msg && msg.messageText && msg.messageText.isSimple) {
            this.setSimpleJob(msg.messageText.module, msg.messageText.function, msg.messageText.payload, msg.messageText.fail, msg.messageText.failArray);
            this.originalMsg = msg;
            this.id = msg.messageText.id;
        } else if (msg && msg.messageText && msg.messageText.isMulti) {
            this.setMultiJobFromMsg(msg.messageText);
            this.originalMsg = msg;
            this.id = msg.messageText.id;
        } else {
            // NOTHING TO DO, NEW OBJECT
        }
    }

    setSimpleJob(moduleName, functionName, payload, fail, failArray) {
        assert.equal(typeof moduleName, 'string', 'module must be a string');
        assert.equal(typeof functionName, 'string', 'function must be a string');
        assert.equal(typeof payload, 'object', 'payload must be a object');
        assert.ok(!this.isMulti, 'already a multi object');
        this.module    = moduleName;
        this.function  = functionName;
        this.payload   = payload;
        this.fail      = fail || 0 ;
        this.failArray = failArray || [];
        this.isSimple  = true;
        return this;
    }

    setMultiJob() {
        assert.ok(!this.isSimple, 'already a simple object');
        this.isMulti = true;
        this.fail = 0;
        this.failArray = [];
        this.stepDone = [];
        this.stepNext = [];
        return this;
    }

    addStep(module, functionName, paramObj){
        assert.equal(typeof module, 'string', 'module must be a string');
        assert.equal(typeof functionName, 'string', 'function must be a string');
        assert.ok(!this.isSimple, 'already a simple object');
        var step = {
            moduleName: module,
            function: functionName,
            payload: paramObj,
            fail: 0
        }
        this.stepNext.push(step);
        return this;
    }

    popStep(){
        assert.ok(!this.isSimple,'the job is a simple obj');
        assert.equal(this.stepNext.length>0,true,'the are no current step');
        var step = this.stepNext[0];
        this.stepNext.splice(0,1); // remove the 0 indexed item
        this.stepDone.push(step);
    }

    updateStepPayload(payload){
        assert.ok(!this.isSimple,'the job is a simple obj');
        if(this.stepNext.length>0) {
            if(payload.postRunFunction){
                payload = funcster.deepDeserialize(payload);
                try {
                    payload = payload.postRunFunction(payload);
                }catch(err){
                    console.error(err);
                }
            }
            this.stepNext[0].payload = payload;
        }
    }

    setMultiJobFromMsg(msgText){
        this.setMultiJob();
        this.fail = msgText.fail || 0;
        this.failArray = msgText.failArray || [];
        this.stepDone = msgText.stepDone;
        this.stepNext = msgText.stepNext;
        return this;
    }

    getMsg(){
        let outMsg;
        if(this.isSimple){
            outMsg = {
                module: this.module,
                function: this.function,
                fail: this.fail,
                isSimple: this.isSimple,
                payload: this.payload,
                failArray: this.failArray
            }
        }else if(this.isMulti){
            outMsg = {
                isMulti: this.isMulti,
                fail: this.fail,
                stepDone: this.stepDone,
                stepNext: this.stepNext,
                failArray: this.failArray
            }
        }
        if(this.reserved && outMsg){
            outMsg.reserved = this.reserved;
            outMsg.dtReserve = this.dtReserve;
        }
        outMsg.id = this.id;
        return outMsg;
    }

    getCompleteMsg(){
        var msg = this.originalMsg || {};
        msg.messageText = this.getMsg();
        return msg;
    }

    getId(){
        var msg = this.originalMsg || {};
        return msg.messageId;
    }

    execute(worker,onComplete){
        if(this.isSimple) {
            this.executeSimple(worker,onComplete);
        }else if (this.isMulti){
            this.executeMulti(worker,onComplete);
        }else{
            // empty for some reason
            console.log(MODULE_NAME + ': aborted empty operation');
            onComplete(null,null);
        }

    }

    executeSimple(worker,onComplete) {
        try {
            let module = this.getModule(this.module);
            console.log(MODULE_NAME + ': executing %s.%s', this.module, this.function);
            module[this.function](this.payload, worker, onComplete);
        } catch (error) {
            // sync errors
            console.error(error);
            onComplete(error,null);
        }
    }

    executeMulti(worker,onComplete) {
        let th = this;
        try{
            async.whilst(
                function isMoreToProcess(){
                    var cond1 = th.stepNext.length>0;  // there is a element to execute
                    var cond2 = !(th.stepNext.suspended); // the step is not suspended
                    return cond1 && cond2;
                },
                function process(callback){
                    th.steplevel = th.steplevel + 1;
                    let step = th.stepNext[0];
                    th.processStep(step,worker,callback);
                },
                function onFinish(err,res){
                    if(err){
                        th.fails(err);
                        worker.updateMsg(th.getCompleteMsg(),onComplete);
                    }else{
                        console.log(MODULE_NAME + ': exiting step mode');
                        onComplete(null,{success:true});
                    }
                }
            );
        }catch( error){
            console.error(error);
            onComplete(error,null);
        }
    }

    processStep(step,worker,callback){
        let th = this;
        try{
            async.waterfall([

                // reserve the object
                function reserve(wfcallback){
                    //th.reserve(worker,wfcallback);
                    // HERE IS ALREADY RESERVED BY WORKER
                    wfcallback(null,null);
                },

                // execute
                function exe(res,wfcallback){
                    let module = th.getModule(step.moduleName);
                    console.log(MODULE_NAME + ': executing %s.%s -  %s', step.moduleName, step.function,th.originalMsg ? th.originalMsg.messageId:'ND');
                    if(step.payload.skipWorker) {
                        module[step.function](step.payload, wfcallback);
                    }else{
                        module[step.function](step.payload, worker, wfcallback);
                    }
                },

                // update and unlock
                function update(paramObj,wfcallback){
                    th.popStep();
                    th.updateStepPayload(paramObj);
                    //worker.updateMsg(th.getCompleteMsg(),wfcallback); // SKIP UPDATE
                    wfcallback();
                },

                // complete step
                function complete(wfcallback){
                    wfcallback();
                }
            ],
                // sum up
            function onComplete(err,res){
                if(err){
                    th.fails(err);
                    worker.updateMsg(th.getCompleteMsg(),callback);
                }else{
                    callback(null,res);
                }
            })
        }catch(err){
            th.fails(err);
            worker.updateMsg(th.getCompleteMsg(),callback);
        }
    }

    /**
     * trace a fail
     * @param err
     */
    fails(err,step){
        assert.equal(typeof err,'object','Error is null');
        this.fail = this.fail +1;
        this.reserved = false;
        try {
            this.failArray.push(util.inspect(err));
        }catch(inspecterr){
            console.error(inspecterr);
        }

        if(step){
            step.fail = step.fail+1;
            if(step.payload && step.payload.postFailFunction && (typeof step.payload.postFailFunction == 'function' )){
                try {
                    step.payload = payload.postFailFunction(payload,step,err);
                }catch(err){
                    console.error(err);
                }

            }
        }
    }

    /**
     * reserve the message
     * @param worker
     * @param onDone
     */
    reserve(worker,onDone){
        this.reserved = true;
        this.dtReserve = new Date();
        this.reserver = worker.name;
        var msg = this.getCompleteMsg();
        if(!msg || !msg.messageId){
            console.log(MODULE_NAME + ': CANNOT RESERVE EMPTY OBJECT ' + util.inspect(msg));
            onDone(null,null);
        }else {
            worker.updateMsg(this.getCompleteMsg(), onDone);
        }
    }

    /**
     * unreserve object
     * @param save
     * @param onDone
     */
    unreserve(save,onDone){
        this.reserved = false;
        if(save){
            var msg = this.getCompleteMsg();
            if(!msg || !msg.messageId){
                console.log(MODULE_NAME + ': CANNOT UNRESERVE EMPTY OBJECT ' + util.inspect(msg));
                onDone(null,null);
            }else {
                worker.updateMsg(this.getCompleteMsg(), onDone);
            }
        }
    }

    /**
     * return a moduble by name/path
     * @param moduleName the pathname
     * @returns a modole itself
     */
    getModule(moduleName){
        assert(typeof moduleName,'string','moduleName must be a string');
        return require(this._getPath(moduleName));
    }

    /**
     * util method to get the path normalized
     * @param inputPath
     * @returns {string}
     */
    _getPath(inputPath){
        assert(typeof inputPath,'string','inputPath must be a string');
        var base = global.__base || __dirname + '/';
        let normalizedPath = path.normalize(base + inputPath);
        return normalizedPath;
    }

}

module.exports = MessageJob;
<commit_msg>limit to fail array<commit_after>/**
 * Created by mox on 23/03/16.
 */

"use strict";

const MODULE_NAME = 'MessageJob';
const OVER_REPETITION_THRSH = 10;
const EventEmitter = require('events');
const util = require('util');
const path = require('path');
const assert = require('assert');
const retry = require('retry');
const async = require('async');
const moment = require('moment');
const funcster = require('funcster');

class MessageJob extends EventEmitter {

    constructor(msg) {
        super();
        if (msg && msg.messageText && msg.messageText.isSimple) {
            this.setSimpleJob(msg.messageText.module, msg.messageText.function, msg.messageText.payload, msg.messageText.fail, msg.messageText.failArray);
            this.originalMsg = msg;
            this.id = msg.messageText.id;
        } else if (msg && msg.messageText && msg.messageText.isMulti) {
            this.setMultiJobFromMsg(msg.messageText);
            this.originalMsg = msg;
            this.id = msg.messageText.id;
        } else {
            // NOTHING TO DO, NEW OBJECT
        }
    }

    setSimpleJob(moduleName, functionName, payload, fail, failArray) {
        assert.equal(typeof moduleName, 'string', 'module must be a string');
        assert.equal(typeof functionName, 'string', 'function must be a string');
        assert.equal(typeof payload, 'object', 'payload must be a object');
        assert.ok(!this.isMulti, 'already a multi object');
        this.module    = moduleName;
        this.function  = functionName;
        this.payload   = payload;
        this.fail      = fail || 0 ;
        this.failArray = failArray || [];
        this.isSimple  = true;
        return this;
    }

    setMultiJob() {
        assert.ok(!this.isSimple, 'already a simple object');
        this.isMulti = true;
        this.fail = 0;
        this.failArray = [];
        this.stepDone = [];
        this.stepNext = [];
        return this;
    }

    addStep(module, functionName, paramObj){
        assert.equal(typeof module, 'string', 'module must be a string');
        assert.equal(typeof functionName, 'string', 'function must be a string');
        assert.ok(!this.isSimple, 'already a simple object');
        var step = {
            moduleName: module,
            function: functionName,
            payload: paramObj,
            fail: 0
        }
        this.stepNext.push(step);
        return this;
    }

    popStep(){
        assert.ok(!this.isSimple,'the job is a simple obj');
        assert.equal(this.stepNext.length>0,true,'the are no current step');
        var step = this.stepNext[0];
        this.stepNext.splice(0,1); // remove the 0 indexed item
        this.stepDone.push(step);
    }

    updateStepPayload(payload){
        assert.ok(!this.isSimple,'the job is a simple obj');
        if(this.stepNext.length>0) {
            if(payload.postRunFunction){
                payload = funcster.deepDeserialize(payload);
                try {
                    payload = payload.postRunFunction(payload);
                }catch(err){
                    console.error(err);
                }
            }
            this.stepNext[0].payload = payload;
        }
    }

    setMultiJobFromMsg(msgText){
        this.setMultiJob();
        this.fail = msgText.fail || 0;
        this.failArray = msgText.failArray || [];
        this.stepDone = msgText.stepDone;
        this.stepNext = msgText.stepNext;
        return this;
    }

    getMsg(){
        let outMsg;
        if(this.isSimple){
            outMsg = {
                module: this.module,
                function: this.function,
                fail: this.fail,
                isSimple: this.isSimple,
                payload: this.payload,
                failArray: this.failArray
            }
        }else if(this.isMulti){
            outMsg = {
                isMulti: this.isMulti,
                fail: this.fail,
                stepDone: this.stepDone,
                stepNext: this.stepNext,
                failArray: this.failArray
            }
        }
        if(this.reserved && outMsg){
            outMsg.reserved = this.reserved;
            outMsg.dtReserve = this.dtReserve;
        }
        outMsg.id = this.id;
        return outMsg;
    }

    getCompleteMsg(){
        var msg = this.originalMsg || {};
        msg.messageText = this.getMsg();
        return msg;
    }

    getId(){
        var msg = this.originalMsg || {};
        return msg.messageId;
    }

    execute(worker,onComplete){
        if(this.isSimple) {
            this.executeSimple(worker,onComplete);
        }else if (this.isMulti){
            this.executeMulti(worker,onComplete);
        }else{
            // empty for some reason
            console.log(MODULE_NAME + ': aborted empty operation');
            onComplete(null,null);
        }

    }

    executeSimple(worker,onComplete) {
        try {
            let module = this.getModule(this.module);
            console.log(MODULE_NAME + ': executing %s.%s', this.module, this.function);
            module[this.function](this.payload, worker, onComplete);
        } catch (error) {
            // sync errors
            console.error(error);
            onComplete(error,null);
        }
    }

    executeMulti(worker,onComplete) {
        let th = this;
        try{
            async.whilst(
                function isMoreToProcess(){
                    var cond1 = th.stepNext.length>0;  // there is a element to execute
                    var cond2 = !(th.stepNext.suspended); // the step is not suspended
                    return cond1 && cond2;
                },
                function process(callback){
                    th.steplevel = th.steplevel + 1;
                    let step = th.stepNext[0];
                    th.processStep(step,worker,callback);
                },
                function onFinish(err,res){
                    if(err){
                        th.fails(err);
                        worker.updateMsg(th.getCompleteMsg(),onComplete);
                    }else{
                        console.log(MODULE_NAME + ': exiting step mode');
                        onComplete(null,{success:true});
                    }
                }
            );
        }catch( error){
            console.error(error);
            onComplete(error,null);
        }
    }

    processStep(step,worker,callback){
        let th = this;
        try{
            async.waterfall([

                // reserve the object
                function reserve(wfcallback){
                    //th.reserve(worker,wfcallback);
                    // HERE IS ALREADY RESERVED BY WORKER
                    wfcallback(null,null);
                },

                // execute
                function exe(res,wfcallback){
                    let module = th.getModule(step.moduleName);
                    console.log(MODULE_NAME + ': executing %s.%s -  %s', step.moduleName, step.function,th.originalMsg ? th.originalMsg.messageId:'ND');
                    if(step.payload.skipWorker) {
                        module[step.function](step.payload, wfcallback);
                    }else{
                        module[step.function](step.payload, worker, wfcallback);
                    }
                },

                // update and unlock
                function update(paramObj,wfcallback){
                    th.popStep();
                    th.updateStepPayload(paramObj);
                    //worker.updateMsg(th.getCompleteMsg(),wfcallback); // SKIP UPDATE
                    wfcallback();
                },

                // complete step
                function complete(wfcallback){
                    wfcallback();
                }
            ],
                // sum up
            function onComplete(err,res){
                if(err){
                    th.fails(err);
                    worker.updateMsg(th.getCompleteMsg(),callback);
                }else{
                    callback(null,res);
                }
            })
        }catch(err){
            th.fails(err);
            worker.updateMsg(th.getCompleteMsg(),callback);
        }
    }

    /**
     * trace a fail
     * @param err
     */
    fails(err,step){
        assert.equal(typeof err,'object','Error is null');
        this.fail = this.fail +1;
        this.reserved = false;
        try {
            this.failArray.push(util.inspect(err));
            if(this.failArray.length>20){
                this.failArray.splice(0,1);
            }
        }catch(inspecterr){
            console.error(inspecterr);
        }

        if(step){
            step.fail = step.fail+1;
            if(step.payload && step.payload.postFailFunction && (typeof step.payload.postFailFunction == 'function' )){
                try {
                    step.payload = payload.postFailFunction(payload,step,err);
                }catch(err){
                    console.error(err);
                }

            }
        }
    }

    /**
     * reserve the message
     * @param worker
     * @param onDone
     */
    reserve(worker,onDone){
        this.reserved = true;
        this.dtReserve = new Date();
        this.reserver = worker.name;
        var msg = this.getCompleteMsg();
        if(!msg || !msg.messageId){
            console.log(MODULE_NAME + ': CANNOT RESERVE EMPTY OBJECT ' + util.inspect(msg));
            onDone(null,null);
        }else {
            worker.updateMsg(this.getCompleteMsg(), onDone);
        }
    }

    /**
     * unreserve object
     * @param save
     * @param onDone
     */
    unreserve(save,onDone){
        this.reserved = false;
        if(save){
            var msg = this.getCompleteMsg();
            if(!msg || !msg.messageId){
                console.log(MODULE_NAME + ': CANNOT UNRESERVE EMPTY OBJECT ' + util.inspect(msg));
                onDone(null,null);
            }else {
                worker.updateMsg(this.getCompleteMsg(), onDone);
            }
        }
    }

    /**
     * return a moduble by name/path
     * @param moduleName the pathname
     * @returns a modole itself
     */
    getModule(moduleName){
        assert(typeof moduleName,'string','moduleName must be a string');
        return require(this._getPath(moduleName));
    }

    /**
     * util method to get the path normalized
     * @param inputPath
     * @returns {string}
     */
    _getPath(inputPath){
        assert(typeof inputPath,'string','inputPath must be a string');
        var base = global.__base || __dirname + '/';
        let normalizedPath = path.normalize(base + inputPath);
        return normalizedPath;
    }

}

module.exports = MessageJob;
<|endoftext|>


<commit_before>'use strict';

YUI.add('juju-view-service', function(Y) {

var ENTER = Y.Node.DOM_EVENTS.key.eventDef.KEY_MAP.enter;
var ESC = Y.Node.DOM_EVENTS.key.eventDef.KEY_MAP.esc;

var views = Y.namespace('juju.views'),
    Templates = views.Templates,
    models = Y.namespace('juju.models');

var exposeButtonMixin = {
    events: {
        '.unexposeService': {click: 'unexposeService'},
        '.exposeService': {click: 'exposeService'}
    },

    unexposeService: function() {
        var service = this.get('service'),
            env = this.get('env');

        env.unexpose(service.get('id'),
            Y.bind(this._unexposeServiceCallback, this));
    },

    _unexposeServiceCallback: function() {
        var service = this.get('service'),
            db = this.get('db');
        service.set('exposed', false);
        db.fire('update');
    },

    exposeService: function() {
        var service = this.get('service'),
            env = this.get('env');
        env.expose(service.get('id'),
            Y.bind(this._exposeServiceCallback, this));
    },

    _exposeServiceCallback: function() {
        var service = this.get('service'),
            db = this.get('db');
        service.set('exposed', true);
        db.fire('update');
    }
};

var ServiceRelations = Y.Base.create('ServiceRelationsView', Y.View, [views.JujuBaseView], {

    template: Templates['service-relations'],

    initializer: function() {
        Y.mix(this, exposeButtonMixin, undefined, undefined, undefined, true);
    },

    render: function() {
        var container = this.get('container'),
            db = this.get('db'),
            service = this.get('service');
        container.setHTML(this.template(
            {'service': service.getAttrs(),
             'relations': service.get('rels'),
             'charm': this.renderable_charm(service.get('charm'), db)}
            ));
    }
});

views.service_relations = ServiceRelations;

var ServiceConstraints = Y.Base.create('ServiceConstraintsView', Y.View, [views.JujuBaseView], {

    template: Templates['service-constraints'],

    initializer: function() {
        Y.mix(this, exposeButtonMixin, undefined, undefined, undefined, true);
    },

    render: function() {
        var container = this.get('container'),
            db = this.get('db'),
            service = this.get('service');

        var constraints = service.get('constraints');
        var display_constraints = [];

        for (var key in constraints) {
            display_constraints.push({'name': key, 'value': constraints[key]});
        }

        var generics = ['cpu', 'mem', 'arch'];
        for (var idx in generics) {
            var gkey = generics[idx];
            if (! (gkey in constraints)) {
                display_constraints.push({'name': gkey, 'value': ''});
            }
        }

        console.log('service constraints', display_constraints);
        container.setHTML(this.template(
            {'service': service.getAttrs(),
             'constraints': display_constraints,
             'charm': this.renderable_charm(service.get('charm'), db)}
            ));
    }

});

views.service_constraints = ServiceConstraints;

var ServiceConfigView = Y.Base.create('ServiceConfigView', Y.View, [views.JujuBaseView], {

    template: Templates['service-config'],

    initializer: function() {
        Y.mix(this, exposeButtonMixin, undefined, undefined, undefined, true);
    },

    events: {
        '#save-service-config': {click: 'saveConfig'},
        '.alert > .close': {click: 'closeAlert'}},

    render: function () {
        var container = this.get('container'),
          db = this.get('db'),
          service = this.get('service');

        if (!service || !service.get('loaded')) {
            console.log('not connected / maybe');
            return this;
        }

        console.log('config', service.get('config'));
        var charm_url = service.get('charm');

        // combine the charm schema and the service values for display.
        var charm = db.charms.getById(charm_url);
        var config = service.get('config');
        var schema = charm.get('config');

        var settings = [];
        var field_def;

        for (var field_name in schema) {
            field_def = schema[field_name];
            settings.push(Y.mix(
                {'name': field_name, 'value': config[field_name]}, field_def));
        }

        console.log('render view svc config', service.getAttrs(), settings);

        container.setHTML(this.template(
            {service: service.getAttrs(),
             settings: settings,
             charm: this.renderable_charm(service.get('charm'), db)}
            ));

        return this;
    },

    saveConfig: function() {
        var env = this.get('env'),
            container = this.get('container'),
            service = this.get('service'),
            config = {};

        // Disable the "Update" button while the RPC call is outstanding.
        container.one('#save-service-config').set('disabled', 'disabled');

        container.all('.config-field').each(function(el) {
            config[el.get('name')] = el.get('value');
        });

        env.set_config(service.get('id'), config,
            Y.bind(this._saveConfigCallback, this));

    },

    _addErrorMessage: function(container, message) {
        container.one('#message-area')
            .appendChild(Y.Node.create('<div/>'))
                .addClass('alert')
                .addClass('alert-error')
                .set('text', message)
            .appendChild(Y.Node.create('<a/>'))
                .addClass('close')
                .set('text', '×');
    },

    _serverErrorMessage: 'An error ocurred.',

    _saveConfigCallback: function(ev) {
        var service = this.get('service'),
            container = this.get('container'),
            db = this.get('db');

        if (ev && ev.err) {
            this._addErrorMessage(container, this._serverErrorMessage);
        } else {
            var config = service.get('config');
            container.all('.config-field').each(function(el) {
                config[el.get('name')] = el.get('value');
            });
        }

        container.one('#save-service-config').removeAttribute('disabled');
    },

    closeAlert: function(ev) {
        ev.target.get('parentNode').remove();
    }
});

views.service_config = ServiceConfigView;

var ServiceView = Y.Base.create('ServiceView', Y.View, [views.JujuBaseView], {

    template: Templates.service,

    initializer: function() {
        Y.mix(this, exposeButtonMixin, undefined, undefined, undefined, true);
    },

    render: function () {
        console.log('service view render');

        var container = this.get('container'),
            db = this.get('db'),
            service = this.get('service'),
            env = this.get('env');

        if (!service) {
            console.log('not connected / maybe');
            return this;
        }
        var units = db.units.get_units_for_service(service);
        var charm_name = service.get('charm');
        container.setHTML(this.template(
            {'service': service.getAttrs(),
             'charm': this.renderable_charm(charm_name, db),
             'units': units.map(function(u) {
                 return u.getAttrs();})
        }));
        return this;
    },

    events: {
        '#num-service-units': {keydown: 'modifyUnits', blur: 'resetUnits'},
        'div.thumbnail': {click: function(ev) {
            console.log('Unit clicked', ev.currentTarget.get('id'));
            this.fire('showUnit', {unit_id: ev.currentTarget.get('id')}),
        'a#destroy-service': {click: 'confirmDestroy'},
        '#destroy-service-modal.btn-danger': {click: 'destroyService'}
        }},
    },

    confirmDestroy: function (ev) {
        // We wait to make the panel until now, because in the render method
        // the container is not yet part of the document.
        if (Y.Lang.isUndefined(this.panel)) {
            var panel = this.panel = new Y.Panel({
                bodyContent: 'Are you sure you want to destroy the service? This cannot be undone.',
                width: 400,
                zIndex: 5,
                centered: true,
                show: false,
                classNames: 'modal',
                modal: true,
                render: '#destroy-modal-panel',
                buttons: [
                    {
                        value  : 'Destroy Service',
                        section: Y.WidgetStdMod.FOOTER,
                        action : Y.bind(this.destroyService, this),
                        classNames: ['btn-danger', 'btn']
                    },
                    {
                        value  : 'Cancel',
                        section: Y.WidgetStdMod.FOOTER,
                        action : function (e) {
                            e.preventDefault();
                            panel.hide();
                        },
                        classNames: ['btn']
                    }
                ]
            });
        }
        this.panel.show();
        // The default YUI CSS conflicts with the CSS effect we want.
        Y.all('#destroy-modal-panel .yui3-button').removeClass('yui3-button');
    },

    destroyService: function(ev) {
        ev.preventDefault();
        var env = this.get('env'),
            service = this.get('model');
        ev.target.set('disabled', true);
        env.destroy_service(
            service.get('id'), Y.bind(this._destroyCallback, this));
    },

    _destroyCallback: function(ev) {
        var db = this.get('db'),
            service = this.get('model'),
            service_id = service.get('id');
        db.services.remove(service);
        db.relations.remove(
            db.relations.filter(
                function (r) {
                    return Y.Array.some(r.get('endpoints'), function (ep) {
                        return ep[0] === service_id;
                    });
                }
            ));
        this.panel.hide();
        this.panel.destroy();
        this.fire('showEnvironment');
    },

    unexposeService: function() {
        var service = this.get('service'),
            env = this.get('env');

        env.unexpose(service.get('id'),
            Y.bind(this._unexposeServiceCallback, this));
    },

    _unexposeServiceCallback: function() {
        var service = this.get('service'),
            db = this.get('db');
        service.set('exposed', false);
        db.fire('update');
    },

    exposeService: function() {
        var service = this.get('service'),
            env = this.get('env');
        env.expose(service.get('id'),
            Y.bind(this._exposeServiceCallback, this));
    },

    _exposeServiceCallback: function() {
        var service = this.get('service'),
            db = this.get('db');
        service.set('exposed', true);
        db.fire('update');
    },

    resetUnits: function(ev) {
        var container = this.get('container'),
            field = container.one('#num-service-units');
        field.set('value', this.get('service').get('unit_count'));
    },

    modifyUnits: function (ev) {
        if (ev.keyCode != ESC && ev.keyCode != ENTER) {
            return;
        }
        var container = this.get('container'),
            field = container.one('#num-service-units');
        if (ev.keyCode == ESC) {
            field.set('value', this.get('service').get('unit_count'));
        }
        if (ev.keyCode != ENTER) { // If not Enter keyup...
            return;
        }
        ev.halt(true);
        this._modifyUnits(parseInt(field.get('value'), 10));
    },

    _modifyUnits: function(requested_unit_count) {

        var service = this.get('service'),
            unit_count = service.get('unit_count'),
            field = this.get('container').one('#num-service-units'),
            env = this.get('env');

        if (requested_unit_count < 1) {
            console.log('You must have at least one unit');
            field.set('value', unit_count);
            return;
        }

        var delta = requested_unit_count - unit_count;
        if (delta > 0) {
            // Add units!
            env.add_unit(
                service.get('id'), delta,
                Y.bind(this._addUnitCallback, this));
        } else if (delta < 0) {
            delta = Math.abs(delta);
            var db = this.get('db'),
                units = db.units.get_units_for_service(service),
                unit_ids_to_remove = [];

            for (var i=units.length - 1;
                 unit_ids_to_remove.length < delta;
                 i--) {
                unit_ids_to_remove.push(units[i].get('id'));
            }
            env.remove_units(
                unit_ids_to_remove,
                Y.bind(this._removeUnitCallback, this)
                );
        }
        field.set('disabled', true);
    },

    _addUnitCallback: function(ev) {
        var service = this.get('service'),
            service_id = service.get('id'),
            db = this.get('db'),
            unit_names = ev.result || [];
        console.log('_addUnitCallback with: ', arguments);
        // Received acknowledgement message for the 'add_units' operation.
        // ev.results is an array of the new unit ids to be created.
        db.units.add(
            Y.Array.map(unit_names, function (unit_id) {
                return new models.ServiceUnit(
                    {id: unit_id,
                     agent_state: 'pending',
                     service: service_id});
            }));
        service.set(
            'unit_count', service.get('unit_count') + unit_names.length);
        db.fire('update');
        // View is redrawn so we do not need to enable field.
    },

    _removeUnitCallback: function(ev) {
        var service = this.get('service'),
            db = this.get('db'),
            unit_names = ev.unit_names;
        console.log('_removeUnitCallback with: ', arguments);
        Y.Array.each(unit_names, function(unit_name) {
            db.units.remove(db.units.getById(unit_name));
        });
        service.set(
            'unit_count', service.get('unit_count') - unit_names.length);
        db.fire('update');
        // View is redrawn so we do not need to enable field.
    }
});

views.service = ServiceView;
}, '0.1.0', {
    requires: ['panel',
               'juju-view-utils',
               'juju-models',
               'base-build',
               'handlebars',
               'node',
               'view',
               'event-key',
               'json-stringify']
});
<commit_msg>only known test fail<commit_after>'use strict';

YUI.add('juju-view-service', function(Y) {

var ENTER = Y.Node.DOM_EVENTS.key.eventDef.KEY_MAP.enter;
var ESC = Y.Node.DOM_EVENTS.key.eventDef.KEY_MAP.esc;

var views = Y.namespace('juju.views'),
    Templates = views.Templates,
    models = Y.namespace('juju.models');

var exposeButtonMixin = {
    events: {
        '.unexposeService': {click: 'unexposeService'},
        '.exposeService': {click: 'exposeService'}
    },

    unexposeService: function() {
        var service = this.get('service'),
            env = this.get('env');

        env.unexpose(service.get('id'),
            Y.bind(this._unexposeServiceCallback, this));
    },

    _unexposeServiceCallback: function() {
        var service = this.get('service'),
            db = this.get('db');
        service.set('exposed', false);
        db.fire('update');
    },

    exposeService: function() {
        var service = this.get('service'),
            env = this.get('env');
        env.expose(service.get('id'),
            Y.bind(this._exposeServiceCallback, this));
    },

    _exposeServiceCallback: function() {
        var service = this.get('service'),
            db = this.get('db');
        service.set('exposed', true);
        db.fire('update');
    }
};

var ServiceRelations = Y.Base.create('ServiceRelationsView', Y.View, [views.JujuBaseView], {

    template: Templates['service-relations'],

    initializer: function() {
        Y.mix(this, exposeButtonMixin, undefined, undefined, undefined, true);
    },

    render: function() {
        var container = this.get('container'),
            db = this.get('db'),
            service = this.get('service');
        container.setHTML(this.template(
            {'service': service.getAttrs(),
             'relations': service.get('rels'),
             'charm': this.renderable_charm(service.get('charm'), db)}
            ));
    }
});

views.service_relations = ServiceRelations;

var ServiceConstraints = Y.Base.create('ServiceConstraintsView', Y.View, [views.JujuBaseView], {

    template: Templates['service-constraints'],

    initializer: function() {
        Y.mix(this, exposeButtonMixin, undefined, undefined, undefined, true);
    },

    render: function() {
        var container = this.get('container'),
            db = this.get('db'),
            service = this.get('service');

        var constraints = service.get('constraints');
        var display_constraints = [];

        for (var key in constraints) {
            display_constraints.push({'name': key, 'value': constraints[key]});
        }

        var generics = ['cpu', 'mem', 'arch'];
        for (var idx in generics) {
            var gkey = generics[idx];
            if (! (gkey in constraints)) {
                display_constraints.push({'name': gkey, 'value': ''});
            }
        }

        console.log('service constraints', display_constraints);
        container.setHTML(this.template(
            {'service': service.getAttrs(),
             'constraints': display_constraints,
             'charm': this.renderable_charm(service.get('charm'), db)}
            ));
    }

});

views.service_constraints = ServiceConstraints;

var ServiceConfigView = Y.Base.create('ServiceConfigView', Y.View, [views.JujuBaseView], {

    template: Templates['service-config'],

    initializer: function() {
        Y.mix(this, exposeButtonMixin, undefined, undefined, undefined, true);
    },

    events: {
        '#save-service-config': {click: 'saveConfig'},
        '.alert > .close': {click: 'closeAlert'}},

    render: function () {
        var container = this.get('container'),
          db = this.get('db'),
          service = this.get('service');

        if (!service || !service.get('loaded')) {
            console.log('not connected / maybe');
            return this;
        }

        console.log('config', service.get('config'));
        var charm_url = service.get('charm');

        // combine the charm schema and the service values for display.
        var charm = db.charms.getById(charm_url);
        var config = service.get('config');
        var schema = charm.get('config');

        var settings = [];
        var field_def;

        for (var field_name in schema) {
            field_def = schema[field_name];
            settings.push(Y.mix(
                {'name': field_name, 'value': config[field_name]}, field_def));
        }

        console.log('render view svc config', service.getAttrs(), settings);

        container.setHTML(this.template(
            {service: service.getAttrs(),
             settings: settings,
             charm: this.renderable_charm(service.get('charm'), db)}
            ));

        return this;
    },

    saveConfig: function() {
        var env = this.get('env'),
            container = this.get('container'),
            service = this.get('service'),
            config = {};

        // Disable the "Update" button while the RPC call is outstanding.
        container.one('#save-service-config').set('disabled', 'disabled');

        container.all('.config-field').each(function(el) {
            config[el.get('name')] = el.get('value');
        });

        env.set_config(service.get('id'), config,
            Y.bind(this._saveConfigCallback, this));

    },

    _addErrorMessage: function(container, message) {
        container.one('#message-area')
            .appendChild(Y.Node.create('<div/>'))
                .addClass('alert')
                .addClass('alert-error')
                .set('text', message)
            .appendChild(Y.Node.create('<a/>'))
                .addClass('close')
                .set('text', '×');
    },

    _serverErrorMessage: 'An error ocurred.',

    _saveConfigCallback: function(ev) {
        var service = this.get('service'),
            container = this.get('container'),
            db = this.get('db');

        if (ev && ev.err) {
            this._addErrorMessage(container, this._serverErrorMessage);
        } else {
            var config = service.get('config');
            container.all('.config-field').each(function(el) {
                config[el.get('name')] = el.get('value');
            });
        }

        container.one('#save-service-config').removeAttribute('disabled');
    },

    closeAlert: function(ev) {
        ev.target.get('parentNode').remove();
    }
});

views.service_config = ServiceConfigView;

var ServiceView = Y.Base.create('ServiceView', Y.View, [views.JujuBaseView], {

    template: Templates.service,

    initializer: function() {
        Y.mix(this, exposeButtonMixin, undefined, undefined, undefined, true);
    },

    render: function () {
        console.log('service view render');

        var container = this.get('container'),
            db = this.get('db'),
            service = this.get('service'),
            env = this.get('env');

        if (!service) {
            console.log('not connected / maybe');
            return this;
        }
        var units = db.units.get_units_for_service(service);
        var charm_name = service.get('charm');
        container.setHTML(this.template(
            {'service': service.getAttrs(),
             'charm': this.renderable_charm(charm_name, db),
             'units': units.map(function(u) {
                 return u.getAttrs();})
        }));
        return this;
    },

    events: {
        '#num-service-units': {keydown: 'modifyUnits', blur: 'resetUnits'},
        'div.thumbnail': {click: function(ev) {
            console.log('Unit clicked', ev.currentTarget.get('id'));
            this.fire('showUnit', {unit_id: ev.currentTarget.get('id')})
        }},
        'a#destroy-service': {click: 'confirmDestroy'},
        '#destroy-service-modal.btn-danger': {click: 'destroyService'}
    },

    confirmDestroy: function (ev) {
        // We wait to make the panel until now, because in the render method
        // the container is not yet part of the document.
        if (Y.Lang.isUndefined(this.panel)) {
            var panel = this.panel = new Y.Panel({
                bodyContent: 'Are you sure you want to destroy the service? This cannot be undone.',
                width: 400,
                zIndex: 5,
                centered: true,
                show: false,
                classNames: 'modal',
                modal: true,
                render: '#destroy-modal-panel',
                buttons: [
                    {
                        value  : 'Destroy Service',
                        section: Y.WidgetStdMod.FOOTER,
                        action : Y.bind(this.destroyService, this),
                        classNames: ['btn-danger', 'btn']
                    },
                    {
                        value  : 'Cancel',
                        section: Y.WidgetStdMod.FOOTER,
                        action : function (e) {
                            e.preventDefault();
                            panel.hide();
                        },
                        classNames: ['btn']
                    }
                ]
            });
        }
        this.panel.show();
        // The default YUI CSS conflicts with the CSS effect we want.
        Y.all('#destroy-modal-panel .yui3-button').removeClass('yui3-button');
    },

    destroyService: function(ev) {
        ev.preventDefault();
        var env = this.get('env'),
            service = this.get('model');
        ev.target.set('disabled', true);
        env.destroy_service(
            service.get('id'), Y.bind(this._destroyCallback, this));
    },

    _destroyCallback: function(ev) {
        var db = this.get('db'),
            service = this.get('model'),
            service_id = service.get('id');
        db.services.remove(service);
        db.relations.remove(
            db.relations.filter(
                function (r) {
                    return Y.Array.some(r.get('endpoints'), function (ep) {
                        return ep[0] === service_id;
                    });
                }
            ));
        this.panel.hide();
        this.panel.destroy();
        this.fire('showEnvironment');
    },

    unexposeService: function() {
        var service = this.get('service'),
            env = this.get('env');

        env.unexpose(service.get('id'),
            Y.bind(this._unexposeServiceCallback, this));
    },

    _unexposeServiceCallback: function() {
        var service = this.get('service'),
            db = this.get('db');
        service.set('exposed', false);
        db.fire('update');
    },

    exposeService: function() {
        var service = this.get('service'),
            env = this.get('env');
        env.expose(service.get('id'),
            Y.bind(this._exposeServiceCallback, this));
    },

    _exposeServiceCallback: function() {
        var service = this.get('service'),
            db = this.get('db');
        service.set('exposed', true);
        db.fire('update');
    },

    resetUnits: function(ev) {
        var container = this.get('container'),
            field = container.one('#num-service-units');
        field.set('value', this.get('service').get('unit_count'));
    },

    modifyUnits: function (ev) {
        if (ev.keyCode != ESC && ev.keyCode != ENTER) {
            return;
        }
        var container = this.get('container'),
            field = container.one('#num-service-units');
        if (ev.keyCode == ESC) {
            field.set('value', this.get('service').get('unit_count'));
        }
        if (ev.keyCode != ENTER) { // If not Enter keyup...
            return;
        }
        ev.halt(true);
        this._modifyUnits(parseInt(field.get('value'), 10));
    },

    _modifyUnits: function(requested_unit_count) {

        var service = this.get('service'),
            unit_count = service.get('unit_count'),
            field = this.get('container').one('#num-service-units'),
            env = this.get('env');

        if (requested_unit_count < 1) {
            console.log('You must have at least one unit');
            field.set('value', unit_count);
            return;
        }

        var delta = requested_unit_count - unit_count;
        if (delta > 0) {
            // Add units!
            env.add_unit(
                service.get('id'), delta,
                Y.bind(this._addUnitCallback, this));
        } else if (delta < 0) {
            delta = Math.abs(delta);
            var db = this.get('db'),
                units = db.units.get_units_for_service(service),
                unit_ids_to_remove = [];

            for (var i=units.length - 1;
                 unit_ids_to_remove.length < delta;
                 i--) {
                unit_ids_to_remove.push(units[i].get('id'));
            }
            env.remove_units(
                unit_ids_to_remove,
                Y.bind(this._removeUnitCallback, this)
                );
        }
        field.set('disabled', true);
    },

    _addUnitCallback: function(ev) {
        var service = this.get('service'),
            service_id = service.get('id'),
            db = this.get('db'),
            unit_names = ev.result || [];
        console.log('_addUnitCallback with: ', arguments);
        // Received acknowledgement message for the 'add_units' operation.
        // ev.results is an array of the new unit ids to be created.
        db.units.add(
            Y.Array.map(unit_names, function (unit_id) {
                return new models.ServiceUnit(
                    {id: unit_id,
                     agent_state: 'pending',
                     service: service_id});
            }));
        service.set(
            'unit_count', service.get('unit_count') + unit_names.length);
        db.fire('update');
        // View is redrawn so we do not need to enable field.
    },

    _removeUnitCallback: function(ev) {
        var service = this.get('service'),
            db = this.get('db'),
            unit_names = ev.unit_names;
        console.log('_removeUnitCallback with: ', arguments);
        Y.Array.each(unit_names, function(unit_name) {
            db.units.remove(db.units.getById(unit_name));
        });
        service.set(
            'unit_count', service.get('unit_count') - unit_names.length);
        db.fire('update');
        // View is redrawn so we do not need to enable field.
    }
});

views.service = ServiceView;
}, '0.1.0', {
    requires: ['panel',
               'juju-view-utils',
               'juju-models',
               'base-build',
               'handlebars',
               'node',
               'view',
               'event-key',
               'json-stringify']
});
<|endoftext|>


<commit_before><commit_msg>* bugfix for dotcover not terminating the build properly on failure & remove dead code in gulp-dotcover.js<commit_after><|endoftext|>


<commit_before>/**
 * UI for search module
 * @author Sergey Chikuyonok (serge.che@gmail.com)
 * @link http://chikuyonok.ru
 *
 * @include "utils.js"
 * @include "dom.js"
 * @include "signals.js"
 * @include "search.js"
 */
(function(){
	var search_index = [],
		/** @type {Document} */
		doc,

		/** @type {Array} List of last matched elements */
		last_search,

		last_query,
		/** @type {Element} Popup container */
		popup,
		max_visible_results = 20,

		selected_item = -1,

		is_visible = false,

		/** @type {Element} Search field */
		search_field,

		hover_lock_timeout,
		is_hover_locked = false;

	/**
	 * Returns list of search result items
	 * @return {NodeList}
	 */
	function getSearchResultItems() {
		return xv_dom.getByClass('xv-search-result-item');
	}

	/**
	 * Select search reult item
	 * @param {Number} ix Index of item
	 * @param {Boolean} no_scroll Do not scroll popup content to make shure that
	 * selected item is always visible
	 */
	function selectItem(ix, no_scroll) {
		var result_items = getSearchResultItems(),
			/** @type {Element} */
			cur_item;

		ix = Math.min(result_items.length - 1, Math.max(0, ix));

		_.each(result_items, function(n, i) {
			xv_dom.removeClass(n, 'xv-search-result-item-selected');
			if (i == ix) {
				cur_item = n;
				xv_dom.addClass(n, 'xv-search-result-item-selected');
			}
		});

		if (!no_scroll && cur_item) {
			var content = cur_item.parentNode;
			// make sure that selected proposal is visible
			var proposal_top = cur_item.offsetTop,
				proposal_height = cur_item.offsetHeight,
				popup_scroll = content.scrollTop,
				popup_height = content.offsetHeight;

			if (proposal_top < popup_scroll) {
				content.scrollTop = proposal_top;
			} else if (proposal_top + proposal_height > popup_scroll + popup_height) {
				content.scrollTop = proposal_top + proposal_height - popup_height;
			}
		}

		selected_item = ix;
		xv_signals.searchItemSelected.dispatch(cur_item, selected_item);
	}

	/**
	 * Temporary lock popup hover events.
	 * Hover lock is used to prevent accident mouseover event callback when
	 * mouse cursor is over popup window and user traverses between proposals
	 * with arrow keys
	 */
	function lockHover() {
		if (hover_lock_timeout)
			clearTimeout(hover_lock_timeout);

		is_hover_locked = true;
		setTimeout(function() {
			is_hover_locked = false;
		}, 100);
	}

	/**
	 * Creates label that will be displayed as a search result
	 * @param {Object} item Search result item
	 * @param {String} query Search query
	 */
	function createSearchResultLabel(item) {
		var label = item.label;
		if (item.query_start != -1) {
			label = item.label.substring(0, item.query_start) +
				'' + item.label.substring(item.query_start, item.query_end) + '' +
				item.label.substring(item.query_end);
		}

		return label + ' ' + item.xpath + '';
	}

	/**
	 * Creates HTML list of found nodes
	 * @param {Array} found List of matched nodes
	 * @param {String} query Search query
	 * @return {jQuery}
	 */
	function buildSearchResult(found, query) {
		var items = _.map(found.results, function(n, i) {
			return '
' + createSearchResultLabel(n) + '
';
		});

		// reset selected item
		selected_item = -1;

		var content = xv_dom.getOneByClass('xv-search-result-content');
		xv_dom.empty(content);
		content.appendChild(xv_dom.fromHTML(items.join('')));

		if (found.results.length > max_visible_results) {
			xv_dom.addClass(popup, 'xv-search-result-overflow');
			xv_dom.setCSS(content, {'height': getSearchResultItems()[0].offsetHeight * max_visible_results});
		} else {
			xv_dom.removeClass(popup, 'xv-search-result-overflow');
			xv_dom.setCSS(content, {'height': 'auto'});
		}
	}

	function hidePopup() {
		xv_dom.addClass(popup, 'xv-search-result-hidden');
		is_visible = false;
	}

	function showPopup() {
		xv_dom.removeClass(popup, 'xv-search-result-hidden');
		is_visible = true;
	}

	function performSearch() {
		var query = xv_utils.trim(search_field.value).toLowerCase();

		if (!query) {
			hidePopup();
			return;
		}

		if (query == last_query && is_visible)
			return;

		last_query = query;
		last_search = xv_search.search(query);

		if (last_search.results) {
			showPopup();
			buildSearchResult(last_search);
		} else {
			hidePopup();
		}
	}

	function applyProposal(ix) {
		xv_search.applyProposal(ix);
		hidePopup();
	}

	var _doSearch = _.debounce(performSearch, 150);

	/**
	 * Handle keyboard event performed on search field
	 * @param {Event} evt
	 */
	function handleKeyEvent(evt) {
		if (is_visible) {
			switch (evt.keyCode) {
				case 38: //up
					selectItem(selected_item - 1);
					lockHover();
					evt.preventDefault();
					return;
				case 40: //down
					selectItem(selected_item + 1);
					lockHover();
					evt.preventDefault();
					return;
				case 13: //enter
					applyProposal(selected_item);
					evt.preventDefault();
					return;
				case 27: // escape
					hidePopup();
					evt.preventDefault();
					return;
			}
		}

		_doSearch();
	}

	function runOnDelegated(cur_elem, fn) {
		var elem = xv_dom.bubbleSearch(cur_elem, 'xv-search-result-item');
		if (elem) {
			var ix = _.indexOf(getSearchResultItems(), elem);
			if (ix != -1)
				fn(ix);
		}
	}

	xv_signals.documentProcessed.addOnce(function() {
		popup = xv_dom.fromHTML('

');

		hidePopup();

		var panel = xv_dom.getOneByClass('xv-search-panel');
		panel.appendChild(popup);

		search_field = xv_dom.getOneByClass('xv-search-field', panel);

		// bind events to search field
		xv_dom.addEvent(search_field, 'keydown', handleKeyEvent);

		// delegate hover event: hilight proposal
		xv_dom.addEvent(popup, 'mouseover', function(/* Event */ evt) {
			if (is_hover_locked) return;
			runOnDelegated(evt.target, function(ix) { selectItem(ix, true); });
		});

		// delegate click event: apply proposal
		xv_dom.addEvent(popup, 'click', function(/* Event */ evt) {
			runOnDelegated(evt.target, function(ix) {
				applyProposal(ix);
				hidePopup();
			});
		});

		var dont_hide = false;

		xv_dom.addEvent(search_field, 'blur', function() {
			// use delayed execution in to handle popup click event correctly
			setTimeout(function() {
				if (!dont_hide) hidePopup();
				dont_hide = false;
			}, 200);
		});

		xv_dom.addEvent(search_field, 'focus', performSearch);

		xv_dom.addEvent(popup, 'mousedown', function(/* Event */ evt) {
			evt.preventDefault();
			evt.stopPropagation();
			dont_hide = true;
			return false;
		});

		xv_dom.addEvent(document, 'mousedown', hidePopup);
	});
})();<commit_msg>Minor tweaks in search UI module<commit_after>/**
 * UI for search module
 * @author Sergey Chikuyonok (serge.che@gmail.com)
 * @link http://chikuyonok.ru
 *
 * @include "utils.js"
 * @include "dom.js"
 * @include "signals.js"
 * @include "search.js"
 */
(function(){
	var search_index = [],
		/** @type {Document} */
		doc,

		/** @type {Array} List of last matched elements */
		last_search,

		last_query,
		/** @type {Element} Popup container */
		popup,
		max_visible_results = 20,

		selected_item = -1,

		is_visible = false,

		/** @type {Element} Search field */
		search_field,

		hover_lock_timeout,
		is_hover_locked = false;

	/**
	 * Returns list of search result items
	 * @return {NodeList}
	 */
	function getSearchResultItems() {
		return xv_dom.getByClass('xv-search-result-item', popup);
	}

	/**
	 * Select search result item
	 * @param {Number} ix Index of item
	 * @param {Boolean} no_scroll Do not scroll popup content to make sure that
	 * selected item is always visible
	 */
	function selectItem(ix, no_scroll) {
		var result_items = getSearchResultItems(),
			/** @type {Element} */
			cur_item;

		ix = Math.min(result_items.length - 1, Math.max(0, ix));

		_.each(result_items, function(n, i) {
			xv_dom.removeClass(n, 'xv-search-result-item-selected');
			if (i == ix) {
				cur_item = n;
				xv_dom.addClass(n, 'xv-search-result-item-selected');
			}
		});

		if (!no_scroll && cur_item) {
			var content = cur_item.parentNode;
			// make sure that selected proposal is visible
			var proposal_top = cur_item.offsetTop,
				proposal_height = cur_item.offsetHeight,
				popup_scroll = content.scrollTop,
				popup_height = content.offsetHeight;

			if (proposal_top < popup_scroll) {
				content.scrollTop = proposal_top;
			} else if (proposal_top + proposal_height > popup_scroll + popup_height) {
				content.scrollTop = proposal_top + proposal_height - popup_height;
			}
		}

		selected_item = ix;
		xv_signals.searchItemSelected.dispatch(cur_item, selected_item);
	}

	/**
	 * Temporary lock popup hover events.
	 * Hover lock is used to prevent accident mouseover event callback when
	 * mouse cursor is over popup window and user traverses between proposals
	 * with arrow keys
	 */
	function lockHover() {
		if (hover_lock_timeout)
			clearTimeout(hover_lock_timeout);

		is_hover_locked = true;
		setTimeout(function() {
			is_hover_locked = false;
		}, 100);
	}

	/**
	 * Creates label that will be displayed as a search result
	 * @param {Object} item Search result item
	 * @param {String} query Search query
	 */
	function createSearchResultLabel(item) {
		var label = item.label;
		if (item.query_start != -1) {
			label = item.label.substring(0, item.query_start) +
				'' + item.label.substring(item.query_start, item.query_end) + '' +
				item.label.substring(item.query_end);
		}

		return label + ' ' + item.xpath + '';
	}

	/**
	 * Creates HTML list of found nodes
	 * @param {Array} found List of matched nodes
	 * @param {String} query Search query
	 * @return {jQuery}
	 */
	function buildSearchResult(found, query) {
		var items = _.map(found.results, function(n, i) {
			return '
' + createSearchResultLabel(n) + '
';
		});

		// reset selected item
		selected_item = -1;

		var content = xv_dom.getOneByClass('xv-search-result-content');
		xv_dom.empty(content);
		content.appendChild(xv_dom.fromHTML(items.join('')));

		if (found.results.length > max_visible_results) {
			xv_dom.addClass(popup, 'xv-search-result-overflow');
			xv_dom.setCSS(content, {'height': getSearchResultItems()[0].offsetHeight * max_visible_results});
		} else {
			xv_dom.removeClass(popup, 'xv-search-result-overflow');
			xv_dom.setCSS(content, {'height': 'auto'});
		}
	}

	function hidePopup() {
		xv_dom.addClass(popup, 'xv-search-result-hidden');
		is_visible = false;
	}

	function showPopup() {
		xv_dom.removeClass(popup, 'xv-search-result-hidden');
		is_visible = true;
	}

	function performSearch() {
		var query = xv_utils.trim(search_field.value).toLowerCase();

		if (!query) {
			hidePopup();
			return;
		}

		if (query == last_query && is_visible)
			return;

		last_query = query;
		last_search = xv_search.search(query);

		if (last_search.results) {
			showPopup();
			buildSearchResult(last_search);
		} else {
			hidePopup();
		}
	}

	function applyProposal(ix) {
		xv_search.applyProposal(ix);
		hidePopup();
	}

	var _doSearch = _.debounce(performSearch, 150);

	/**
	 * Handle keyboard event performed on search field
	 * @param {Event} evt
	 */
	function handleKeyEvent(evt) {
		if (is_visible) {
			switch (evt.keyCode) {
				case 38: //up
					selectItem(selected_item - 1);
					lockHover();
					evt.preventDefault();
					return;
				case 40: //down
					selectItem(selected_item + 1);
					lockHover();
					evt.preventDefault();
					return;
				case 13: //enter
					applyProposal(selected_item);
					evt.preventDefault();
					return;
				case 27: // escape
					hidePopup();
					evt.preventDefault();
					return;
			}
		}

		_doSearch();
	}

	function runOnDelegated(cur_elem, fn) {
		var elem = xv_dom.bubbleSearch(cur_elem, 'xv-search-result-item');
		if (elem) {
			var ix = _.indexOf(getSearchResultItems(), elem);
			if (ix != -1)
				fn(ix);
		}
	}

	xv_signals.documentProcessed.addOnce(function() {
		popup = xv_dom.fromHTML('

');

		hidePopup();

		var panel = xv_dom.getOneByClass('xv-search-panel');
		panel.appendChild(popup);

		search_field = xv_dom.getOneByClass('xv-search-field', panel);

		// bind events to search field
		xv_dom.addEvent(search_field, 'keydown', handleKeyEvent);

		// delegate hover event: hilight proposal
		xv_dom.addEvent(popup, 'mouseover', function(/* Event */ evt) {
			if (is_hover_locked) return;
			runOnDelegated(evt.target, function(ix) { selectItem(ix, true); });
		});

		// delegate click event: apply proposal
		xv_dom.addEvent(popup, 'click', function(/* Event */ evt) {
			runOnDelegated(evt.target, function(ix) {
				applyProposal(ix);
				hidePopup();
			});
		});

		var dont_hide = false;

		xv_dom.addEvent(search_field, 'blur', function() {
			// use delayed execution in to handle popup click event correctly
			setTimeout(function() {
				if (!dont_hide) hidePopup();
				dont_hide = false;
			}, 200);
		});

		xv_dom.addEvent(search_field, 'focus', performSearch);

		xv_dom.addEvent(popup, 'mousedown', function(/* Event */ evt) {
			evt.preventDefault();
			evt.stopPropagation();
			dont_hide = true;
			return false;
		});

		xv_dom.addEvent(document, 'mousedown', hidePopup);
	});
})();<|endoftext|>