<commit_before>//routes.js

var exports = module.exports = {};

var _  = require('lodash'),
fuzzy  = require('fuzzy'),
os     = require('os'),

moment = require('moment'),
parse  = require('app/shiftswap/parse'),

sms    = require('app/shiftswap/sms'),
db     = require('app/shiftswap/db'),

serverConfig = require('app/_config/server.json')
;

exports.home = {
    welcome: (req, res)=>{
        res.send("\"Welcome. You have connected to the Shiftswap API.\"");
        //res.send(JSON.stringify(parse.date.unixToday()));
    },
    time: (req, res)=>{
        var m = moment.utc();
        res.send({
            human:   m.format('dddd, MMMM Do YYYY, h:mm:ss a'),
            unix:    m.valueOf(),
            iso8601: m.toISOString(),
        });
    },
    echo: (req, res)=>{
        res.send(req.params.str);
    },
    whoami: (req, res)=>{
        res.send({
            ip:     req.connection.remoteAddress || req.ip,
            host:   req.headers['user-agent'],
            cookie: req.headers['cookie'],
            dnt:    req.headers['dnt'],
        });
    },
};
exports.user = {
    post: (req, res)=>{
        var post = req.body;
        console.log("POST user");

        //Validate
        post.access = post.access || "f";

        //Parsing
        post.access = minifyAccess(post.access, post.spectator);

        if(post.spectator)
            post.access.push("s");

        function createUserDB(err, uid){
            new model.User({
                name:   post.full   || "",
                nick:   post.nick   || "",
                access: post.access || ["f"],
                period: post.period || [Date.now()],
            }).save(callback);
        }
        function callback(err, obj){
            res.send({stat:201,res:"Created"});
            logHistory("Normal|Created user "+obj.name, obj, null);
        }
    },
    del: (req, res)=>{
        var uid = JSON.parse(req.params.uid);
        console.log('DELETE user '+uid);
        logHistory("Very High|Deleted user "+uid, null, {_id: uid}, callback);

        function callback(){
            model.User.remove({_id: uid}, () => res.send({state:200, res:'Ok'}) );
        }
    },
    all: (req, res)=>{
        console.log('GET users');
        db.users.getAll(callback);

        function callback(err, list){
            list = parse.json.cleanArray(list);
            res.send(list);
        }
    },
    patch: (req, res)=>{
        var uid;
        try{
            uid = JSON.parse(req.params.uid) || req.params.uid;
        }
        catch(e){
            uid = req.params.uid;
        }

        var patch = req.body;
        console.log('PATCH user '+uid);

        patch.access = minifyAccess(patch.access);

        model.User.findOneAndUpdate({_id: uid}, patch, {upsert: false}, function(err, doc){
            if(err){
                res.send({state:500, res:'Internal error: '+err});
                return console.error(err);
            }

            res.send({state:200, res:'Ok'});
            logHistory("Normal|Editted user "+doc.name, patch, doc);
        });
    },

    userid: (req, res)=>{

        var userid = JSON.parse(req.params.uid);
        if(!users.some( obj => {
                if(obj.userid === userid){
                    res.send('
'+JSON.stringify(obj)+'
');
                    return true;
                }
        })){
            res.send('
No userid matches '+userid+'
');
        }
    },
};
exports.stats = {
    get: (req, res)=>{
        var a = parse.military.pad(14, 30);
        console.log(a);
        res.send(a);
    },
    put: (req, res)=>{

    },
    delete: (req, res)=>{

    },
};
exports.schedule = {
    post: (req, res)=>{
        console.log('POST schedule');
        var post = req.body || {};

        if(post.jquery){
            post = post.jquery;
        }

        db.schedule.insert(post, (err, doc)=>{
            if(err){
                console.error(err);
                return res.send(JSON.stringify(err));
            }

            res.send(JSON.stringify('saved'));
        });
    },
    userid: (req, res)=>{

    },
    get: (req, res)=>{
        var day = req.params.day;
        var uid = req.params.uid;
        var access = req.params.access;

        var find;

        if(!day){
            //No day selected, send full list.
            console.log('day empty');
            find = {};
        }
        else{
            //Day found, send partial.
            console.log('GET schedule',day.human, day.unix);
            find = {'date.unix': day.unix};
        }

        if(!day)
            day = parse.date.unixToday();
        if(uid)
            find.uid = uid;
        if(access)
            find.access = access[0];

        db.schedule.getBetweenDates(day, -7, find, function(err, docs){
            var list = [];

            if(err)
                return console.error(err);

            docs.forEach((d)=>{
                d=parse.json.clean(d); //clean
                list.push(d); //copy
            });

            res.send(list); //send
        });

    },
    delete: (req, res)=>{

    },
};
exports.logged = {
    never: (req, res)=>{

    },
    last: (req, res)=>{

    },
};
exports.trade = {
    all: (req, res)=>{
        model.Trade.find({}, (err, docs)=>{
            if(err)
                return console.error(err);

            sortByStatus(docs);
        });

        function sortByStatus(docs){
            docs = docs || [];
            var statusList = {
                pending:  [],
                complete: [],
                fail:     [],
            };
            for(var i = 0, len = docs.length; i < len; i++){
                var obj = docs[i];
                switch(obj.status){
                    case 0: statusList.pending.push(obj);  break;
                    case 1: statusList.complete.push(obj); break;
                    case 2: statusList.fail.push(obj);     break;
                }
            }
            res.send(statusList);
        }
    },
    post: (req, res)=>{
        var post = req.body || {sid: -1, uid: -1};

        if(!post.sid) return console.error('scheduleId is required');
        if(!post.uid) return console.error('userId is required');

        new model.Trade({
            sid: post.sid,
            uid: post.uid,
            term: post.term,
            status: 0,
        }).save(callback);

        function callback(err, doc){
            if(err){
                res.send({state: 500, res: err});
                return console.error(err);
            }

            res.send({state: 200, res: 'Ok', doc: doc});
        }
    },
    take: (req, res)=>{
        var tid = parse.json.loose(req.params.tid);
    },
    admin: (req, res)=>{
        var tid = parse.json.loose(req.params.tid);
    },
};
exports.history = {
    all: (req, res)=>{
        console.log('GET history');
        model.History.find({}, (err, docs)=>{
            if(err)
                return console.error(err);

            res.send(docs);
        });

    },
    comment: (req, res)=>{

    },
    self: (req, res)=>{

    },
    userid: (req, res)=>{

    },
};
exports.reward = {
    all: (req, res)=>{

    },
    take: (req, res)=>{

    },
    edit: (req, res)=>{

    },
};
exports.sms = {
    poll: (req, res)=>{
        console.log('POST sms');

        var number = req.body.From;
        var body   = req.body.Body;

        if(!number){
            var msg = 'SMS rejected because no phone number given.';
            res.status(400).send(msg);
            return console.error(msg);
        }

        var sent = sms.parseIncoming(req, res, db, parse, {
            number: number,
            body: body,
        });
        console.log(`Num: ${number}, Recv: ${body}, Send: ${sent}`);

        res.sendStatus(200);
    },
    send: (req, res)=>{
        console.log('GET sms');
        var body   = req.params.msg;
        //var body   = JSON.parse(req.params.msg);
        var number = JSON.parse(req.params.number);
        sms.send(number, body, function(err, smsRes){
            if(err)
                return console.error('Sms failure', err);
            console.log('sms successful.');
        });
    },
}
exports.voice = {

}

function getUserFromName(name, callback){
    name = (typeof name==='string')? name : '';

    name = name.toLowerCase();
    var names = [];

    getUsers(f1);
    function f1(list){
        names = list;
        var results = fuzzy.filter(name, names, {extract: e=>e.name});

        if(results[0] && results[0].original)
            results = results[0].original;

        if(callback)
            callback(results);
    }
}
function getUsers(callback){
    var list = [];
    model.User.find(function(err, users){
        if(err)
            return console.error(err);

        users.forEach((user)=>list.push(user));
        callback(list);
    });
}

function minifyAccess(str1, str2, str3){
    str = str1 + str2 + str3;
    var types = ["floor","manager","gm","admin","spec"];
    var out = [];
    //Must contain one of these types
    types.forEach(function(ty){
        if(str.toLowerCase().includes(ty)){
            var small = ty[0];
            out.push(small);
        }
    });
    return out;
}
function createID(type, callback){
    console.log('create ID type ',type);
    var t = this;
    var id, found, update;
    type = type.toLowerCase();
    callback = callback || function(){};

    if(!['u','s','h'].some(o=>type===o))
         return console.error('In createID type '+type+' does not match an ID type.  Types are u,s,h.');

    model.Site.find({}, function(err, f){

        if(err)
            return console.error(err);

        //is empty, make new IDs
        if(f.length === 0){
            new model.Site({
                uidCount: 1,
                hidCount: 1,
                sidCount: 1,
            }).save();
            callback(null, 0);

        //is not empty, return ID and increment it.
        }else{
            f = f[0];

            id = ++f[type+'idCount'];
            update = {[type+'idCount']: f[type+'idCount']};

            callback(null, id);
        }

    }).then(doc=>{
        model.Site.findOneAndUpdate(doc, update, {upsert: false}, function(err, doc){
            if(err)
                return console.error('Couldn\'t update site IDs ', err);
        });
    });
}

function logHistory(typedescr, change, undo, callback){

    //Split description and weight
    typedescr  = typedescr.split("|");
    var weight = typedescr.shift();
    var descr  = typedescr.join("|");
    console.log(typedescr);

    //Convert undo id's to actual data
    if(_.find(Object.keys(undo || {}), p => p === '_id')){ //undo is a user id, find the user.
        model.User.findOne(undo, function(err, found){
            if(err)
                return console.error(err);
            createDoc(found);
        });
    }
    else{
        createDoc();
    }

    //Save the history change
    function createDoc(altUndo){
        new model.History({
            //hid:    getID.h();
            //uid:      Schema.Types.ObjectId,
            time:     Date.now(),
            weight: weight,
            descr: descr,
            comment: "",
            change: change,  //the data after the change
            undo: altUndo || undo,
        }).save(callback);
    }

    //Converts MM-DD-YYYY and MM-DD to Unix timestamps
    function HumanToUNIX(human){
        human = string(human);
        if(human.length <= 4){ //MM-DD format
            human = human+String(new Date().getFullYear()); //append year
        }

        if(human.length <= 8){ //MM-DD-YYYY format
            human = String(Date.parse(human)); //convert to UNIX timestamp
        }

        return human;
    }
}
<commit_msg>Made HTTP response work with Twilio server<commit_after>//routes.js

var exports = module.exports = {};

var _  = require('lodash'),
fuzzy  = require('fuzzy'),
os     = require('os'),

moment = require('moment'),
parse  = require('app/shiftswap/parse'),

sms    = require('app/shiftswap/sms'),
db     = require('app/shiftswap/db'),

serverConfig = require('app/_config/server.json')
;

exports.home = {
    welcome: (req, res)=>{
        res.send("\"Welcome. You have connected to the Shiftswap API.\"");
        //res.send(JSON.stringify(parse.date.unixToday()));
    },
    time: (req, res)=>{
        var m = moment.utc();
        res.send({
            human:   m.format('dddd, MMMM Do YYYY, h:mm:ss a'),
            unix:    m.valueOf(),
            iso8601: m.toISOString(),
        });
    },
    echo: (req, res)=>{
        res.send(req.params.str);
    },
    whoami: (req, res)=>{
        res.send({
            ip:     req.connection.remoteAddress || req.ip,
            host:   req.headers['user-agent'],
            cookie: req.headers['cookie'],
            dnt:    req.headers['dnt'],
        });
    },
};
exports.user = {
    post: (req, res)=>{
        var post = req.body;
        console.log("POST user");

        //Validate
        post.access = post.access || "f";

        //Parsing
        post.access = minifyAccess(post.access, post.spectator);

        if(post.spectator)
            post.access.push("s");

        function createUserDB(err, uid){
            new model.User({
                name:   post.full   || "",
                nick:   post.nick   || "",
                access: post.access || ["f"],
                period: post.period || [Date.now()],
            }).save(callback);
        }
        function callback(err, obj){
            res.send({stat:201,res:"Created"});
            logHistory("Normal|Created user "+obj.name, obj, null);
        }
    },
    del: (req, res)=>{
        var uid = JSON.parse(req.params.uid);
        console.log('DELETE user '+uid);
        logHistory("Very High|Deleted user "+uid, null, {_id: uid}, callback);

        function callback(){
            model.User.remove({_id: uid}, () => res.send({state:200, res:'Ok'}) );
        }
    },
    all: (req, res)=>{
        console.log('GET users');
        db.users.getAll(callback);

        function callback(err, list){
            list = parse.json.cleanArray(list);
            res.send(list);
        }
    },
    patch: (req, res)=>{
        var uid;
        try{
            uid = JSON.parse(req.params.uid) || req.params.uid;
        }
        catch(e){
            uid = req.params.uid;
        }

        var patch = req.body;
        console.log('PATCH user '+uid);

        patch.access = minifyAccess(patch.access);

        model.User.findOneAndUpdate({_id: uid}, patch, {upsert: false}, function(err, doc){
            if(err){
                res.send({state:500, res:'Internal error: '+err});
                return console.error(err);
            }

            res.send({state:200, res:'Ok'});
            logHistory("Normal|Editted user "+doc.name, patch, doc);
        });
    },

    userid: (req, res)=>{

        var userid = JSON.parse(req.params.uid);
        if(!users.some( obj => {
                if(obj.userid === userid){
                    res.send('
'+JSON.stringify(obj)+'
');
                    return true;
                }
        })){
            res.send('
No userid matches '+userid+'
');
        }
    },
};
exports.stats = {
    get: (req, res)=>{
        var a = parse.military.pad(14, 30);
        console.log(a);
        res.send(a);
    },
    put: (req, res)=>{

    },
    delete: (req, res)=>{

    },
};
exports.schedule = {
    post: (req, res)=>{
        console.log('POST schedule');
        var post = req.body || {};

        if(post.jquery){
            post = post.jquery;
        }

        db.schedule.insert(post, (err, doc)=>{
            if(err){
                console.error(err);
                return res.send(JSON.stringify(err));
            }

            res.send(JSON.stringify('saved'));
        });
    },
    userid: (req, res)=>{

    },
    get: (req, res)=>{
        var day = req.params.day;
        var uid = req.params.uid;
        var access = req.params.access;

        var find;

        if(!day){
            //No day selected, send full list.
            console.log('day empty');
            find = {};
        }
        else{
            //Day found, send partial.
            console.log('GET schedule',day.human, day.unix);
            find = {'date.unix': day.unix};
        }

        if(!day)
            day = parse.date.unixToday();
        if(uid)
            find.uid = uid;
        if(access)
            find.access = access[0];

        db.schedule.getBetweenDates(day, -7, find, function(err, docs){
            var list = [];

            if(err)
                return console.error(err);

            docs.forEach((d)=>{
                d=parse.json.clean(d); //clean
                list.push(d); //copy
            });

            res.send(list); //send
        });

    },
    delete: (req, res)=>{

    },
};
exports.logged = {
    never: (req, res)=>{

    },
    last: (req, res)=>{

    },
};
exports.trade = {
    all: (req, res)=>{
        model.Trade.find({}, (err, docs)=>{
            if(err)
                return console.error(err);

            sortByStatus(docs);
        });

        function sortByStatus(docs){
            docs = docs || [];
            var statusList = {
                pending:  [],
                complete: [],
                fail:     [],
            };
            for(var i = 0, len = docs.length; i < len; i++){
                var obj = docs[i];
                switch(obj.status){
                    case 0: statusList.pending.push(obj);  break;
                    case 1: statusList.complete.push(obj); break;
                    case 2: statusList.fail.push(obj);     break;
                }
            }
            res.send(statusList);
        }
    },
    post: (req, res)=>{
        var post = req.body || {sid: -1, uid: -1};

        if(!post.sid) return console.error('scheduleId is required');
        if(!post.uid) return console.error('userId is required');

        new model.Trade({
            sid: post.sid,
            uid: post.uid,
            term: post.term,
            status: 0,
        }).save(callback);

        function callback(err, doc){
            if(err){
                res.send({state: 500, res: err});
                return console.error(err);
            }

            res.send({state: 200, res: 'Ok', doc: doc});
        }
    },
    take: (req, res)=>{
        var tid = parse.json.loose(req.params.tid);
    },
    admin: (req, res)=>{
        var tid = parse.json.loose(req.params.tid);
    },
};
exports.history = {
    all: (req, res)=>{
        console.log('GET history');
        model.History.find({}, (err, docs)=>{
            if(err)
                return console.error(err);

            res.send(docs);
        });

    },
    comment: (req, res)=>{

    },
    self: (req, res)=>{

    },
    userid: (req, res)=>{

    },
};
exports.reward = {
    all: (req, res)=>{

    },
    take: (req, res)=>{

    },
    edit: (req, res)=>{

    },
};
exports.sms = {
    poll: (req, res)=>{
        console.log('POST sms');

        var number = req.body.From;
        var body   = req.body.Body;

        if(!number){
            var msg = 'SMS rejected because no phone number given.';
            res.status(400).send(msg);
            return console.error(msg);
        }

        var sent = sms.parseIncoming(req, res, db, parse, {
            number: number,
            body: body,
        }, function(err, send){
            console.log(`Num: ${number}, Recv: ${body}, Send: ${send}`);
            res.set('Content-Type','text/plain'); //https://www.twilio.com/docs/api/twiml/sms/your_response
            res.status(200).send(send);
        });
    },
    send: (req, res)=>{
        console.log('GET sms');
        var body   = req.params.msg;
        //var body   = JSON.parse(req.params.msg);
        var number = JSON.parse(req.params.number);
        sms.send(number, body, function(err, smsRes){
            if(err)
                return console.error('Sms failure', err);
            console.log('sms successful.');
        });
    },
}
exports.voice = {

}

function getUserFromName(name, callback){
    name = (typeof name==='string')? name : '';

    name = name.toLowerCase();
    var names = [];

    getUsers(f1);
    function f1(list){
        names = list;
        var results = fuzzy.filter(name, names, {extract: e=>e.name});

        if(results[0] && results[0].original)
            results = results[0].original;

        if(callback)
            callback(results);
    }
}
function getUsers(callback){
    var list = [];
    model.User.find(function(err, users){
        if(err)
            return console.error(err);

        users.forEach((user)=>list.push(user));
        callback(list);
    });
}

function minifyAccess(str1, str2, str3){
    str = str1 + str2 + str3;
    var types = ["floor","manager","gm","admin","spec"];
    var out = [];
    //Must contain one of these types
    types.forEach(function(ty){
        if(str.toLowerCase().includes(ty)){
            var small = ty[0];
            out.push(small);
        }
    });
    return out;
}
function createID(type, callback){
    console.log('create ID type ',type);
    var t = this;
    var id, found, update;
    type = type.toLowerCase();
    callback = callback || function(){};

    if(!['u','s','h'].some(o=>type===o))
         return console.error('In createID type '+type+' does not match an ID type.  Types are u,s,h.');

    model.Site.find({}, function(err, f){

        if(err)
            return console.error(err);

        //is empty, make new IDs
        if(f.length === 0){
            new model.Site({
                uidCount: 1,
                hidCount: 1,
                sidCount: 1,
            }).save();
            callback(null, 0);

        //is not empty, return ID and increment it.
        }else{
            f = f[0];

            id = ++f[type+'idCount'];
            update = {[type+'idCount']: f[type+'idCount']};

            callback(null, id);
        }

    }).then(doc=>{
        model.Site.findOneAndUpdate(doc, update, {upsert: false}, function(err, doc){
            if(err)
                return console.error('Couldn\'t update site IDs ', err);
        });
    });
}

function logHistory(typedescr, change, undo, callback){

    //Split description and weight
    typedescr  = typedescr.split("|");
    var weight = typedescr.shift();
    var descr  = typedescr.join("|");
    console.log(typedescr);

    //Convert undo id's to actual data
    if(_.find(Object.keys(undo || {}), p => p === '_id')){ //undo is a user id, find the user.
        model.User.findOne(undo, function(err, found){
            if(err)
                return console.error(err);
            createDoc(found);
        });
    }
    else{
        createDoc();
    }

    //Save the history change
    function createDoc(altUndo){
        new model.History({
            //hid:    getID.h();
            //uid:      Schema.Types.ObjectId,
            time:     Date.now(),
            weight: weight,
            descr: descr,
            comment: "",
            change: change,  //the data after the change
            undo: altUndo || undo,
        }).save(callback);
    }

    //Converts MM-DD-YYYY and MM-DD to Unix timestamps
    function HumanToUNIX(human){
        human = string(human);
        if(human.length <= 4){ //MM-DD format
            human = human+String(new Date().getFullYear()); //append year
        }

        if(human.length <= 8){ //MM-DD-YYYY format
            human = String(Date.parse(human)); //convert to UNIX timestamp
        }

        return human;
    }
}
<|endoftext|>


<commit_before>import cockpit from "cockpit";
import QUnit from "qunit-tests";

QUnit.test("format", function (assert) {
    assert.equal(cockpit.format("My $adj message with ${amount} of things", { adj: "special", amount: "lots" }),
                 "My special message with lots of things", "named keys");
    assert.equal(cockpit.format("My $0 message with $1 of things", ["special", "lots"]),
                 "My special message with lots of things", "number keys");
    assert.equal(cockpit.format("My $0 message with $1 of things", "special", "lots"),
                 "My special message with lots of things", "vararg keys");
    assert.equal(cockpit.format("My $0 message with lots of things", "special"),
                 "My special message with lots of things", "vararg one key");
    assert.equal(cockpit.format("Undefined $value", { }), "Undefined ", "missing value");

    /* All falsy values except `0` should return the empty string */
    assert.equal(cockpit.format("$0", 0), "0", "`0` as argument");
    assert.equal(cockpit.format("$0", 0.0), "0", "`0.0` as argument");
    assert.equal(cockpit.format("$0", false), "", "`false` as argument");
    assert.equal(cockpit.format("$0", null), "", "`null` as argument");
});

QUnit.test("format_number", function (assert) {
    const checks = [
        [23.4, "23.4", "23,4"],
        [23.46, "23.5", "23,5"],
        [23.44, "23.4", "23,4"],

        [-23.4, "-23.4", "-23,4"],
        [-23.46, "-23.5", "-23,5"],
        [-23.44, "-23.4", "-23,4"],

        [0, "0", "0"],
        [0.001, "0.001", "0,001"],
        [-0.001, "-0.001", "-0,001"],
        // smaller values get rounded up
        [0.0003, "0.001", "0,001"],
        [-0.0003, "-0.001", "-0,001"],

        [123.0, "123", "123"],
        [123.01, "123", "123"],
        [-123.0, "-123", "-123"],
        [-123.01, "-123", "-123"],
        [null, "", ""],
        [undefined, "", ""],
    ];

    const saved_language = cockpit.language;

    assert.expect(checks.length * 3 + 7);

    cockpit.language = 'en';
    for (let i = 0; i < checks.length; i++) {
        assert.strictEqual(cockpit.format_number(checks[i][0]), checks[i][1],
                           "format_number@en(" + checks[i][0] + ") = " + checks[i][1]);
    }

    cockpit.language = 'de';
    for (let i = 0; i < checks.length; i++) {
        assert.strictEqual(cockpit.format_number(checks[i][0]), checks[i][2],
                           "format_number@de(" + checks[i][0] + ") = " + checks[i][2]);
    }

    cockpit.language = 'pt_BR';
    for (let i = 0; i < checks.length; i++) {
        assert.strictEqual(cockpit.format_number(checks[i][0]), checks[i][2],
                           "format_number@pt_BR(" + checks[i][0] + ") = " + checks[i][2]);
    }

    /* restore this as not to break the other tests */
    cockpit.language = saved_language;

    // custom precision
    assert.strictEqual(cockpit.format_number(1.23456, 2), "1.2", "format_number@en(precision 2)");
    assert.strictEqual(cockpit.format_number(-1.23456, 2), "-1.2", "format_number@en(negative, precision 2)");
    assert.strictEqual(cockpit.format_number(0.23456, 2), "0.23", "format_number@en(precision 2)");
    assert.strictEqual(cockpit.format_number(1.23456, 4), "1.235", "format_number@en(precision 4)");
    assert.strictEqual(cockpit.format_number(0.23456, 4), "0.2346", "format_number@en(precision 4)");
    assert.strictEqual(cockpit.format_number(0.000123, 2), "0.01", "format_number@en(very small, precision 2)");
    assert.strictEqual(cockpit.format_number(-0.000123, 2), "-0.01", "format_number@en(negative, very small, precision 2)");
});

QUnit.test("format_bytes", function (assert) {
    const checks = [
        [999, 1000, "999"],
        [1934, undefined, "1.89 KiB"],
        [1934, 1000, "1.93 KB"],
        [2000, 1024, "1.95 KiB"],
        [1999, 1000, "2.00 KB"],
        [1999, 1024, "1.95 KiB"],
        [1000000, 1000, "1 MB"],
        [1000001, 1000, "1.00 MB"],
        [1000000, 1024, "977 KiB"],
        [2000000, 1024, "1.91 MiB"],
        [2000000, 1000, "2 MB"],
        [2000001, 1000, "2.00 MB"],
        [2000000, "MB", "2 MB"],
        [2000000, "MiB", "1.91 MiB"],
        [2000000, "KB", "2000 KB"],
        [2000000, "KiB", "1953 KiB"],
        [1, "KB", "0.001 KB"],
        [0, "KB", "0 KB"],
        [undefined, "KB", ""],
        [null, "KB", ""],
    ];

    assert.expect(checks.length * 2 + 2);
    for (let i = 0; i < checks.length; i++) {
        assert.strictEqual(cockpit.format_bytes(checks[i][0], checks[i][1]), checks[i][2],
                           "format_bytes(" + checks[i][0] + ", " + String(checks[i][1]) + ") = " + checks[i][2]);
    }
    for (let i = 0; i < checks.length; i++) {
        const split = checks[i][2].split(" ");
        assert.deepEqual(cockpit.format_bytes(checks[i][0], checks[i][1], { separate: true }), split,
                         "format_bytes(" + checks[i][0] + ", " + String(checks[i][1]) + ", true) = " + split);
    }

    // backwards compatible API: format_bytes with a boolean options (used to be a single "separate" flag)
    assert.strictEqual(cockpit.format_bytes(2500000, 1000, false), "2.50 MB");
    assert.deepEqual(cockpit.format_bytes(2500000, 1000, true), ["2.50", "MB"]);
});

QUnit.test("get_byte_units", function (assert) {
    const mib = 1024 * 1024;
    const gib = mib * 1024;
    const tib = gib * 1024;

    const mib_unit = { factor: mib, name: "MiB" };
    const gib_unit = { factor: gib, name: "GiB" };
    const tib_unit = { factor: tib, name: "TiB" };

    function selected(unit) {
        return { factor: unit.factor, name: unit.name, selected: true };
    }

    const checks = [
        [0 * mib, 1024, [selected(mib_unit), gib_unit, tib_unit]],
        [20 * mib, 1024, [selected(mib_unit), gib_unit, tib_unit]],
        [200 * mib, 1024, [selected(mib_unit), gib_unit, tib_unit]],
        [2000 * mib, 1024, [selected(mib_unit), gib_unit, tib_unit]],
        [20000 * mib, 1024, [mib_unit, selected(gib_unit), tib_unit]],
        [20 * gib, 1024, [mib_unit, selected(gib_unit), tib_unit]],
        [200 * gib, 1024, [mib_unit, selected(gib_unit), tib_unit]],
        [2000 * gib, 1024, [mib_unit, selected(gib_unit), tib_unit]],
        [20000 * gib, 1024, [mib_unit, gib_unit, selected(tib_unit)]]
    ];

    assert.expect(checks.length);
    for (let i = 0; i < checks.length; i++) {
        assert.deepEqual(cockpit.get_byte_units(checks[i][0], checks[i][1]), checks[i][2],
                         "get_byte_units(" + checks[i][0] + ", " + checks[i][1] + ") = " + JSON.stringify(checks[i][2]));
    }
});

QUnit.test("format_bytes_per_sec", function (assert) {
    const checks = [
        [2555, "2.50 KiB/s"]
    ];

    assert.expect(checks.length + 9);
    for (let i = 0; i < checks.length; i++) {
        assert.strictEqual(cockpit.format_bytes_per_sec(checks[i][0]), checks[i][1],
                           "format_bytes_per_sec(" + checks[i][0] + ") = " + checks[i][1]);
    }

    // base-10 units
    assert.strictEqual(cockpit.format_bytes_per_sec(2555, 1000), "2.56 kB/s");
    assert.strictEqual(cockpit.format_bytes_per_sec(12345678, 1000), "12.3 MB/s");
    assert.strictEqual(cockpit.format_bytes_per_sec(12345678, "kB/s"), "12346 kB/s");

    // custom precision
    assert.strictEqual(cockpit.format_bytes_per_sec(2555, 1000, { precision: 2 }), "2.6 kB/s");
    assert.strictEqual(cockpit.format_bytes_per_sec(25555, "MB/s", { precision: 2 }), "0.026 MB/s");
    // significant integer digits exceed custom precision
    assert.strictEqual(cockpit.format_bytes_per_sec(25555000, "kB/s", { precision: 2 }), "25555 kB/s");
    assert.strictEqual(cockpit.format_bytes_per_sec(25555678, "kB/s", { precision: 2 }), "25556 kB/s");

    // separate unit
    assert.deepEqual(cockpit.format_bytes_per_sec(2555, undefined, { separate: true }),
                     ["2.50", "KiB/s"]);
    // backwards compatible API for separate flag
    assert.deepEqual(cockpit.format_bytes_per_sec(2555, undefined, true),
                     ["2.50", "KiB/s"]);
});

QUnit.test("format_bits_per_sec", function (assert) {
    const checks = [
        [55, "55 bps"],
        [55.23456789, "55.2 bps"],
        [55.98765432, "56.0 bps"],
        [2555, "2.56 Kbps"],
        [2000, "2 Kbps"],
        [2003, "2.00 Kbps"]
    ];

    assert.expect(checks.length);
    for (let i = 0; i < checks.length; i++) {
        assert.strictEqual(cockpit.format_bits_per_sec(checks[i][0]), checks[i][1],
                           "format_bits_per_sec(" + checks[i][0] + ") = " + checks[i][1]);
    }
});

QUnit.start();
<commit_msg>base1: Clean up and strenghten format_bytes_per_sec test<commit_after>import cockpit from "cockpit";
import QUnit from "qunit-tests";

QUnit.test("format", function (assert) {
    assert.equal(cockpit.format("My $adj message with ${amount} of things", { adj: "special", amount: "lots" }),
                 "My special message with lots of things", "named keys");
    assert.equal(cockpit.format("My $0 message with $1 of things", ["special", "lots"]),
                 "My special message with lots of things", "number keys");
    assert.equal(cockpit.format("My $0 message with $1 of things", "special", "lots"),
                 "My special message with lots of things", "vararg keys");
    assert.equal(cockpit.format("My $0 message with lots of things", "special"),
                 "My special message with lots of things", "vararg one key");
    assert.equal(cockpit.format("Undefined $value", { }), "Undefined ", "missing value");

    /* All falsy values except `0` should return the empty string */
    assert.equal(cockpit.format("$0", 0), "0", "`0` as argument");
    assert.equal(cockpit.format("$0", 0.0), "0", "`0.0` as argument");
    assert.equal(cockpit.format("$0", false), "", "`false` as argument");
    assert.equal(cockpit.format("$0", null), "", "`null` as argument");
});

QUnit.test("format_number", function (assert) {
    const checks = [
        [23.4, "23.4", "23,4"],
        [23.46, "23.5", "23,5"],
        [23.44, "23.4", "23,4"],

        [-23.4, "-23.4", "-23,4"],
        [-23.46, "-23.5", "-23,5"],
        [-23.44, "-23.4", "-23,4"],

        [0, "0", "0"],
        [0.001, "0.001", "0,001"],
        [-0.001, "-0.001", "-0,001"],
        // smaller values get rounded up
        [0.0003, "0.001", "0,001"],
        [-0.0003, "-0.001", "-0,001"],

        [123.0, "123", "123"],
        [123.01, "123", "123"],
        [-123.0, "-123", "-123"],
        [-123.01, "-123", "-123"],
        [null, "", ""],
        [undefined, "", ""],
    ];

    const saved_language = cockpit.language;

    assert.expect(checks.length * 3 + 7);

    cockpit.language = 'en';
    for (let i = 0; i < checks.length; i++) {
        assert.strictEqual(cockpit.format_number(checks[i][0]), checks[i][1],
                           "format_number@en(" + checks[i][0] + ") = " + checks[i][1]);
    }

    cockpit.language = 'de';
    for (let i = 0; i < checks.length; i++) {
        assert.strictEqual(cockpit.format_number(checks[i][0]), checks[i][2],
                           "format_number@de(" + checks[i][0] + ") = " + checks[i][2]);
    }

    cockpit.language = 'pt_BR';
    for (let i = 0; i < checks.length; i++) {
        assert.strictEqual(cockpit.format_number(checks[i][0]), checks[i][2],
                           "format_number@pt_BR(" + checks[i][0] + ") = " + checks[i][2]);
    }

    /* restore this as not to break the other tests */
    cockpit.language = saved_language;

    // custom precision
    assert.strictEqual(cockpit.format_number(1.23456, 2), "1.2", "format_number@en(precision 2)");
    assert.strictEqual(cockpit.format_number(-1.23456, 2), "-1.2", "format_number@en(negative, precision 2)");
    assert.strictEqual(cockpit.format_number(0.23456, 2), "0.23", "format_number@en(precision 2)");
    assert.strictEqual(cockpit.format_number(1.23456, 4), "1.235", "format_number@en(precision 4)");
    assert.strictEqual(cockpit.format_number(0.23456, 4), "0.2346", "format_number@en(precision 4)");
    assert.strictEqual(cockpit.format_number(0.000123, 2), "0.01", "format_number@en(very small, precision 2)");
    assert.strictEqual(cockpit.format_number(-0.000123, 2), "-0.01", "format_number@en(negative, very small, precision 2)");
});

QUnit.test("format_bytes", function (assert) {
    const checks = [
        [999, 1000, "999"],
        [1934, undefined, "1.89 KiB"],
        [1934, 1000, "1.93 KB"],
        [2000, 1024, "1.95 KiB"],
        [1999, 1000, "2.00 KB"],
        [1999, 1024, "1.95 KiB"],
        [1000000, 1000, "1 MB"],
        [1000001, 1000, "1.00 MB"],
        [1000000, 1024, "977 KiB"],
        [2000000, 1024, "1.91 MiB"],
        [2000000, 1000, "2 MB"],
        [2000001, 1000, "2.00 MB"],
        [2000000, "MB", "2 MB"],
        [2000000, "MiB", "1.91 MiB"],
        [2000000, "KB", "2000 KB"],
        [2000000, "KiB", "1953 KiB"],
        [1, "KB", "0.001 KB"],
        [0, "KB", "0 KB"],
        [undefined, "KB", ""],
        [null, "KB", ""],
    ];

    assert.expect(checks.length * 2 + 2);
    for (let i = 0; i < checks.length; i++) {
        assert.strictEqual(cockpit.format_bytes(checks[i][0], checks[i][1]), checks[i][2],
                           "format_bytes(" + checks[i][0] + ", " + String(checks[i][1]) + ") = " + checks[i][2]);
    }
    for (let i = 0; i < checks.length; i++) {
        const split = checks[i][2].split(" ");
        assert.deepEqual(cockpit.format_bytes(checks[i][0], checks[i][1], { separate: true }), split,
                         "format_bytes(" + checks[i][0] + ", " + String(checks[i][1]) + ", true) = " + split);
    }

    // backwards compatible API: format_bytes with a boolean options (used to be a single "separate" flag)
    assert.strictEqual(cockpit.format_bytes(2500000, 1000, false), "2.50 MB");
    assert.deepEqual(cockpit.format_bytes(2500000, 1000, true), ["2.50", "MB"]);
});

QUnit.test("get_byte_units", function (assert) {
    const mib = 1024 * 1024;
    const gib = mib * 1024;
    const tib = gib * 1024;

    const mib_unit = { factor: mib, name: "MiB" };
    const gib_unit = { factor: gib, name: "GiB" };
    const tib_unit = { factor: tib, name: "TiB" };

    function selected(unit) {
        return { factor: unit.factor, name: unit.name, selected: true };
    }

    const checks = [
        [0 * mib, 1024, [selected(mib_unit), gib_unit, tib_unit]],
        [20 * mib, 1024, [selected(mib_unit), gib_unit, tib_unit]],
        [200 * mib, 1024, [selected(mib_unit), gib_unit, tib_unit]],
        [2000 * mib, 1024, [selected(mib_unit), gib_unit, tib_unit]],
        [20000 * mib, 1024, [mib_unit, selected(gib_unit), tib_unit]],
        [20 * gib, 1024, [mib_unit, selected(gib_unit), tib_unit]],
        [200 * gib, 1024, [mib_unit, selected(gib_unit), tib_unit]],
        [2000 * gib, 1024, [mib_unit, selected(gib_unit), tib_unit]],
        [20000 * gib, 1024, [mib_unit, gib_unit, selected(tib_unit)]]
    ];

    assert.expect(checks.length);
    for (let i = 0; i < checks.length; i++) {
        assert.deepEqual(cockpit.get_byte_units(checks[i][0], checks[i][1]), checks[i][2],
                         "get_byte_units(" + checks[i][0] + ", " + checks[i][1] + ") = " + JSON.stringify(checks[i][2]));
    }
});

QUnit.test("format_bytes_per_sec", function (assert) {
    const checks = [
        // default unit
        [5, undefined, undefined, "5 B/s"],
        [2555, undefined, undefined, "2.50 KiB/s"],
        [12345678, undefined, undefined, "11.8 MiB/s"],
        // explicit base-2 unit
        [2555, 1024, undefined, "2.50 KiB/s"],
        // explicit base-10 unit
        [2555, 1000, undefined, "2.56 kB/s"],
        [12345678, 1000, undefined, "12.3 MB/s"],
        // explicit unit
        [12345678, "kB/s", undefined, "12346 kB/s"],
        [12345678, "MiB/s", undefined, "11.8 MiB/s"],
        // custom precision
        [2555, 1000, { precision: 2 }, "2.6 kB/s"],
        [25555, "MB/s", { precision: 2 }, "0.026 MB/s"],
        // significant integer digits exceed custom precision
        [25555000, "kB/s", { precision: 2 }, "25555 kB/s"],
        [25555678, "kB/s", { precision: 2 }, "25556 kB/s"],
    ];

    assert.expect(checks.length + 2);
    for (let i = 0; i < checks.length; i++) {
        assert.strictEqual(cockpit.format_bytes_per_sec(checks[i][0], checks[i][1], checks[i][2]), checks[i][3],
                           `format_bytes_per_sec(${checks[i][0]}, ${checks[i][1]}, ${checks[i][2]}) = ${checks[i][3]}`);
    }

    // separate unit
    assert.deepEqual(cockpit.format_bytes_per_sec(2555, 1024, { separate: true }),
                     ["2.50", "KiB/s"]);
    // backwards compatible API for separate flag
    assert.deepEqual(cockpit.format_bytes_per_sec(2555, 1024, true),
                     ["2.50", "KiB/s"]);
});

QUnit.test("format_bits_per_sec", function (assert) {
    const checks = [
        [55, "55 bps"],
        [55.23456789, "55.2 bps"],
        [55.98765432, "56.0 bps"],
        [2555, "2.56 Kbps"],
        [2000, "2 Kbps"],
        [2003, "2.00 Kbps"]
    ];

    assert.expect(checks.length);
    for (let i = 0; i < checks.length; i++) {
        assert.strictEqual(cockpit.format_bits_per_sec(checks[i][0]), checks[i][1],
                           "format_bits_per_sec(" + checks[i][0] + ") = " + checks[i][1]);
    }
});

QUnit.start();
<|endoftext|>


<commit_before>import { AnnotationCommand } from 'substance'
import {
  BasePackage, EditorBasePackage, ModelComponentPackage, FindAndReplacePackage
} from '../../kit'

import ArticleNavPackage from '../ArticleNavPackage'
import ArticleToolbarPackage from '../shared/ArticleToolbarPackage'
import PersistencePackage from '../../PersistencePackage'
import EntityLabelsPackage from '../shared/EntityLabelsPackage'
import ManuscriptContentPackage from '../shared/ManuscriptContentPackage'

import AddReferenceWorkflow from '../shared/AddReferenceWorkflow'
import AddEntityCommand from './AddEntityCommand'
import ArticleInformationSectionComponent from './ArticleInformationSectionComponent'
import ArticleMetadataComponent from './ArticleMetadataComponent'
import BibliographicEntryEditor from './BibliographicEntryEditor'
import { MoveCollectionItemCommand, RemoveCollectionItemCommand } from './CollectionCommands'
import {
  AddFigurePanelCommand, MoveFigurePanelCommand,
  ReplaceFigurePanelImageCommand, RemoveFigurePanelCommand
} from '../shared/FigurePanelCommands'
import { InsertFigurePanelTool, ReplaceFigurePanelTool } from '../shared/FigurePanelTools'
import FiguresSectionComponent from './FiguresSectionComponent'
import InsertFootnoteCommand from '../shared/InsertFootnoteCommand'
import MetadataSection from './MetadataSection'
import TableFigureComponent from '../shared/TableFigureComponent'
import TranslatableEntryEditor from './TranslatableEntryEditor'
import {
  AddCustomMetadataFieldCommand, MoveCustomMetadataFieldCommand, RemoveCustomMetadataFieldCommand
} from '../shared/CustomMetadataFieldCommands'

export default {
  name: 'ArticleMetadata',
  configure (config) {
    config.import(BasePackage)
    config.import(EditorBasePackage)
    config.import(ArticleToolbarPackage)
    config.import(ArticleNavPackage)
    config.import(PersistencePackage)
    config.import(ManuscriptContentPackage)
    config.import(ModelComponentPackage)
    config.import(EntityLabelsPackage)
    config.import(FindAndReplacePackage)

    config.addComponent('add-reference', AddReferenceWorkflow)
    config.addComponent('article-metadata', ArticleMetadataComponent)
    config.addComponent('article-information', ArticleInformationSectionComponent)
    config.addComponent('bibr', BibliographicEntryEditor, true)
    config.addComponent('insert-reference', AddReferenceWorkflow)
    config.addComponent('table-figure', TableFigureComponent, true)
    config.addComponent('subject', TranslatableEntryEditor)
    config.addComponent('keyword', TranslatableEntryEditor)
    // Note: @figures and @tables are (dynamic) collections derived from the article's content
    config.addComponent('@figures', FiguresSectionComponent)
    config.addComponent('@tables', MetadataSection)

    // Commands
    config.addCommand('add-custom-metadata-field', AddCustomMetadataFieldCommand, {
      commandGroup: 'custom-metadata-fields'
    })
    config.addCommand('add-figure-panel', AddFigurePanelCommand, {
      commandGroup: 'figure-panel'
    })
    config.addCommand('move-down-col-item', MoveCollectionItemCommand, {
      direction: 'down',
      commandGroup: 'collection'
    })
    config.addCommand('move-down-custom-metadata-field', MoveCustomMetadataFieldCommand, {
      direction: 'down',
      commandGroup: 'custom-metadata-fields'
    })
    config.addCommand('move-down-figure-panel', MoveFigurePanelCommand, {
      direction: 'down',
      commandGroup: 'figure-panel'
    })
    config.addCommand('move-up-col-item', MoveCollectionItemCommand, {
      direction: 'up',
      commandGroup: 'collection'
    })
    config.addCommand('move-up-custom-metadata-field', MoveCustomMetadataFieldCommand, {
      direction: 'up',
      commandGroup: 'custom-metadata-fields'
    })
    config.addCommand('move-up-figure-panel', MoveFigurePanelCommand, {
      direction: 'up',
      commandGroup: 'figure-panel'
    })
    config.addCommand('remove-col-item', RemoveCollectionItemCommand, {
      commandGroup: 'collection'
    })
    config.addCommand('remove-custom-metadata-field', RemoveCustomMetadataFieldCommand, {
      commandGroup: 'custom-metadata-fields'
    })
    config.addCommand('remove-figure-panel', RemoveFigurePanelCommand, {
      commandGroup: 'figure-panel'
    })
    config.addCommand('replace-figure-panel-image', ReplaceFigurePanelImageCommand, {
      commandGroup: 'figure-panel'
    })
    config.addCommand('toggle-bold', AnnotationCommand, {
      nodeType: 'bold',
      accelerator: 'CommandOrControl+B',
      commandGroup: 'formatting'
    })
    config.addCommand('toggle-italic', AnnotationCommand, {
      nodeType: 'italic',
      accelerator: 'CommandOrControl+I',
      commandGroup: 'formatting'
    })
    config.addCommand('toggle-monospace', AnnotationCommand, {
      nodeType: 'monospace',
      commandGroup: 'formatting'
    })
    config.addCommand('toggle-subscript', AnnotationCommand, {
      nodeType: 'sub',
      commandGroup: 'formatting'
    })
    config.addCommand('toggle-superscript', AnnotationCommand, {
      nodeType: 'sup',
      commandGroup: 'formatting'
    })

    // Tools
    config.addTool('add-figure-panel', InsertFigurePanelTool)
    config.addTool('replace-figure-panel-image', ReplaceFigurePanelTool)

    // KeyboardShortcuts
    config.addKeyboardShortcut('CommandOrControl+Alt+Up', { command: 'move-up-col-item' })
    config.addKeyboardShortcut('CommandOrControl+Alt+Down', { command: 'move-down-col-item' })
    config.addKeyboardShortcut('CommandOrControl+Alt+Delete', { command: 'remove-col-item' })

    // Labels
    config.addLabel('article', 'Article Information')
    config.addLabel('article-information', 'Article Information')
    config.addLabel('authors', 'Authors')
    config.addLabel('figures', 'Figures')
    config.addLabel('footnotes', 'Footnotes')
    config.addLabel('groups', 'Groups')
    config.addLabel('keywords', 'Keywords')
    config.addLabel('subjects', 'Subjects')
    config.addLabel('organisations', 'Affiliations')
    config.addLabel('references', 'References')
    config.addLabel('tables', 'Tables')
    config.addLabel('translations', 'Translations')
    config.addLabel('add-reference-title', 'Add Reference(s)')
    config.addLabel('add-ref-manually', 'Or create manually')
    config.addLabel('fetch-datacite', 'Fetch from DataCite')
    config.addLabel('enter-doi-placeholder', 'Enter one or more DOIs')
    config.addLabel('doi-fetch-action', 'Add')
    config.addLabel('import-refs', 'Import')
    config.addLabel('supported-ref-formats', 'Supported formats')
    config.addLabel('original-translation', 'Original')
    config.addLabel('add-translation', 'Add Translation')
    config.addLabel('select-language', 'Select language')
    config.addLabel('add', 'Add')
    config.addLabel('edit', 'Edit')
    config.addLabel('remove', 'Remove')
    config.addLabel('workflows', 'Workflows')
    config.addLabel('select-license', 'Select license')
    config.addLabel('select-item', 'Choose')
    config.addLabel('move-down-figure-panel', 'Move Down Sub-Figure')
    config.addLabel('enter-custom-field-name', 'Enter name')
    config.addLabel('enter-custom-field-value', 'Enter value')
    config.addLabel('article-metadata', 'Article Metadata')
    config.addLabel('subtitle', 'Subtitle')
    // Icons
    config.addIcon('move-down-figure-panel', { 'fontawesome': 'fa-caret-square-o-down' })

    // TODO: need to rethink this a some point
    registerCollectionCommand(config, 'author', ['metadata', 'authors'], { keyboardShortcut: 'CommandOrControl+Alt+A', nodeType: 'person' })
    registerCollectionCommand(config, 'award', ['metadata', 'awards'], { keyboardShortcut: 'CommandOrControl+Alt+Y' })
    registerCollectionCommand(config, 'editor', ['metadata', 'editors'], { keyboardShortcut: 'CommandOrControl+Alt+E', nodeType: 'person' })
    registerCollectionCommand(config, 'footnote', ['article', 'footnotes'], { automaticOrder: true, Command: InsertFootnoteCommand })
    registerCollectionCommand(config, 'group', ['metadata', 'groups'], { keyboardShortcut: 'CommandOrControl+Alt+G' })
    registerCollectionCommand(config, 'keyword', ['metadata', 'keywords'], { keyboardShortcut: 'CommandOrControl+Alt+K' })
    registerCollectionCommand(config, 'organisation', ['metadata', 'organisations'], { keyboardShortcut: 'CommandOrControl+Alt+O' })
    registerCollectionCommand(config, 'subject', ['metadata', 'subjects'])
    config.addCommand('insert-reference', AddEntityCommand, {
      workflow: 'add-reference',
      commandGroup: 'add-entity',
      type: 'bibr'
    })
  }
}

// TODO: this is like an overkill, registering one collection item command
// for every collection type
// it would be better to have just one set of commands
// which is detecting the collection automagically
// The challenge will then be how to control certain things,
// such as disabling move up/down etc.
function registerCollectionCommand (config, itemType, collectionPath, options = {}) {
  let nodeType = options.nodeType || itemType
  let xpathSelector = collectionPath.join('.')
  let Command = options.Command || AddEntityCommand
  config.addCommand(`insert-${itemType}`, Command, {
    type: nodeType,
    collection: collectionPath,
    commandGroup: 'add-entity'
  })
  if (options.keyboardShortcut) {
    config.addKeyboardShortcut(options.keyboardShortcut, { command: `add-${itemType}` })
  }
  if (!config.automaticOrder) {
    config.addCommand(`move-up-${itemType}`, MoveCollectionItemCommand, {
      direction: 'up',
      commandGroup: 'collection',
      xpathSelector
    })
    config.addIcon(`move-up-${itemType}`, { 'fontawesome': 'fa-caret-square-o-up' })
    config.addLabel(`move-up-${itemType}`, {
      en: 'Move item up'
    })
    config.addKeyboardShortcut('CommandOrControl+Alt+Up', { command: `move-up-${itemType}` })
    config.addCommand(`move-down-${itemType}`, MoveCollectionItemCommand, {
      direction: 'down',
      commandGroup: 'collection',
      xpathSelector
    })
    config.addIcon(`move-down-${itemType}`, { 'fontawesome': 'fa-caret-square-o-down' })
    config.addLabel(`move-down-${itemType}`, {
      en: 'Move item down'
    })
    config.addKeyboardShortcut('CommandOrControl+Alt+Down', { command: `move-down-${itemType}` })
    config.addCommand(`remove-${itemType}`, RemoveCollectionItemCommand, {
      commandGroup: 'collection',
      xpathSelector
    })

    config.addIcon('remove', {'fontawesome': 'fa-trash'})
    config.addIcon('checked-item', { 'fontawesome': 'fa-check-square-o' })
    config.addIcon('unchecked-item', { 'fontawesome': 'fa-square-o' })
  }
  config.addIcon(`remove-${itemType}`, { 'fontawesome': 'fa-trash' })
  config.addLabel(`remove-${itemType}`, {
    en: 'Remove item'
  })
  config.addKeyboardShortcut('CommandOrControl+Alt+Delete', { command: `remove-${itemType}` })
}
<commit_msg>Add label for issueTitle<commit_after>import { AnnotationCommand } from 'substance'
import {
  BasePackage, EditorBasePackage, ModelComponentPackage, FindAndReplacePackage
} from '../../kit'

import ArticleNavPackage from '../ArticleNavPackage'
import ArticleToolbarPackage from '../shared/ArticleToolbarPackage'
import PersistencePackage from '../../PersistencePackage'
import EntityLabelsPackage from '../shared/EntityLabelsPackage'
import ManuscriptContentPackage from '../shared/ManuscriptContentPackage'

import AddReferenceWorkflow from '../shared/AddReferenceWorkflow'
import AddEntityCommand from './AddEntityCommand'
import ArticleInformationSectionComponent from './ArticleInformationSectionComponent'
import ArticleMetadataComponent from './ArticleMetadataComponent'
import BibliographicEntryEditor from './BibliographicEntryEditor'
import { MoveCollectionItemCommand, RemoveCollectionItemCommand } from './CollectionCommands'
import {
  AddFigurePanelCommand, MoveFigurePanelCommand,
  ReplaceFigurePanelImageCommand, RemoveFigurePanelCommand
} from '../shared/FigurePanelCommands'
import { InsertFigurePanelTool, ReplaceFigurePanelTool } from '../shared/FigurePanelTools'
import FiguresSectionComponent from './FiguresSectionComponent'
import InsertFootnoteCommand from '../shared/InsertFootnoteCommand'
import MetadataSection from './MetadataSection'
import TableFigureComponent from '../shared/TableFigureComponent'
import TranslatableEntryEditor from './TranslatableEntryEditor'
import {
  AddCustomMetadataFieldCommand, MoveCustomMetadataFieldCommand, RemoveCustomMetadataFieldCommand
} from '../shared/CustomMetadataFieldCommands'

export default {
  name: 'ArticleMetadata',
  configure (config) {
    config.import(BasePackage)
    config.import(EditorBasePackage)
    config.import(ArticleToolbarPackage)
    config.import(ArticleNavPackage)
    config.import(PersistencePackage)
    config.import(ManuscriptContentPackage)
    config.import(ModelComponentPackage)
    config.import(EntityLabelsPackage)
    config.import(FindAndReplacePackage)

    config.addComponent('add-reference', AddReferenceWorkflow)
    config.addComponent('article-metadata', ArticleMetadataComponent)
    config.addComponent('article-information', ArticleInformationSectionComponent)
    config.addComponent('bibr', BibliographicEntryEditor, true)
    config.addComponent('insert-reference', AddReferenceWorkflow)
    config.addComponent('table-figure', TableFigureComponent, true)
    config.addComponent('subject', TranslatableEntryEditor)
    config.addComponent('keyword', TranslatableEntryEditor)
    // Note: @figures and @tables are (dynamic) collections derived from the article's content
    config.addComponent('@figures', FiguresSectionComponent)
    config.addComponent('@tables', MetadataSection)

    // Commands
    config.addCommand('add-custom-metadata-field', AddCustomMetadataFieldCommand, {
      commandGroup: 'custom-metadata-fields'
    })
    config.addCommand('add-figure-panel', AddFigurePanelCommand, {
      commandGroup: 'figure-panel'
    })
    config.addCommand('move-down-col-item', MoveCollectionItemCommand, {
      direction: 'down',
      commandGroup: 'collection'
    })
    config.addCommand('move-down-custom-metadata-field', MoveCustomMetadataFieldCommand, {
      direction: 'down',
      commandGroup: 'custom-metadata-fields'
    })
    config.addCommand('move-down-figure-panel', MoveFigurePanelCommand, {
      direction: 'down',
      commandGroup: 'figure-panel'
    })
    config.addCommand('move-up-col-item', MoveCollectionItemCommand, {
      direction: 'up',
      commandGroup: 'collection'
    })
    config.addCommand('move-up-custom-metadata-field', MoveCustomMetadataFieldCommand, {
      direction: 'up',
      commandGroup: 'custom-metadata-fields'
    })
    config.addCommand('move-up-figure-panel', MoveFigurePanelCommand, {
      direction: 'up',
      commandGroup: 'figure-panel'
    })
    config.addCommand('remove-col-item', RemoveCollectionItemCommand, {
      commandGroup: 'collection'
    })
    config.addCommand('remove-custom-metadata-field', RemoveCustomMetadataFieldCommand, {
      commandGroup: 'custom-metadata-fields'
    })
    config.addCommand('remove-figure-panel', RemoveFigurePanelCommand, {
      commandGroup: 'figure-panel'
    })
    config.addCommand('replace-figure-panel-image', ReplaceFigurePanelImageCommand, {
      commandGroup: 'figure-panel'
    })
    config.addCommand('toggle-bold', AnnotationCommand, {
      nodeType: 'bold',
      accelerator: 'CommandOrControl+B',
      commandGroup: 'formatting'
    })
    config.addCommand('toggle-italic', AnnotationCommand, {
      nodeType: 'italic',
      accelerator: 'CommandOrControl+I',
      commandGroup: 'formatting'
    })
    config.addCommand('toggle-monospace', AnnotationCommand, {
      nodeType: 'monospace',
      commandGroup: 'formatting'
    })
    config.addCommand('toggle-subscript', AnnotationCommand, {
      nodeType: 'sub',
      commandGroup: 'formatting'
    })
    config.addCommand('toggle-superscript', AnnotationCommand, {
      nodeType: 'sup',
      commandGroup: 'formatting'
    })

    // Tools
    config.addTool('add-figure-panel', InsertFigurePanelTool)
    config.addTool('replace-figure-panel-image', ReplaceFigurePanelTool)

    // KeyboardShortcuts
    config.addKeyboardShortcut('CommandOrControl+Alt+Up', { command: 'move-up-col-item' })
    config.addKeyboardShortcut('CommandOrControl+Alt+Down', { command: 'move-down-col-item' })
    config.addKeyboardShortcut('CommandOrControl+Alt+Delete', { command: 'remove-col-item' })

    // Labels
    config.addLabel('article', 'Article Information')
    config.addLabel('article-information', 'Article Information')
    config.addLabel('authors', 'Authors')
    config.addLabel('figures', 'Figures')
    config.addLabel('footnotes', 'Footnotes')
    config.addLabel('groups', 'Groups')
    config.addLabel('issueTitle', 'Issue Title')
    config.addLabel('keywords', 'Keywords')
    config.addLabel('subjects', 'Subjects')
    config.addLabel('organisations', 'Affiliations')
    config.addLabel('references', 'References')
    config.addLabel('tables', 'Tables')
    config.addLabel('translations', 'Translations')
    config.addLabel('add-reference-title', 'Add Reference(s)')
    config.addLabel('add-ref-manually', 'Or create manually')
    config.addLabel('fetch-datacite', 'Fetch from DataCite')
    config.addLabel('enter-doi-placeholder', 'Enter one or more DOIs')
    config.addLabel('doi-fetch-action', 'Add')
    config.addLabel('import-refs', 'Import')
    config.addLabel('supported-ref-formats', 'Supported formats')
    config.addLabel('original-translation', 'Original')
    config.addLabel('add-translation', 'Add Translation')
    config.addLabel('select-language', 'Select language')
    config.addLabel('add', 'Add')
    config.addLabel('edit', 'Edit')
    config.addLabel('remove', 'Remove')
    config.addLabel('workflows', 'Workflows')
    config.addLabel('select-license', 'Select license')
    config.addLabel('select-item', 'Choose')
    config.addLabel('move-down-figure-panel', 'Move Down Sub-Figure')
    config.addLabel('enter-custom-field-name', 'Enter name')
    config.addLabel('enter-custom-field-value', 'Enter value')
    config.addLabel('article-metadata', 'Article Metadata')
    config.addLabel('subtitle', 'Subtitle')
    // Icons
    config.addIcon('move-down-figure-panel', { 'fontawesome': 'fa-caret-square-o-down' })

    // TODO: need to rethink this a some point
    registerCollectionCommand(config, 'author', ['metadata', 'authors'], { keyboardShortcut: 'CommandOrControl+Alt+A', nodeType: 'person' })
    registerCollectionCommand(config, 'award', ['metadata', 'awards'], { keyboardShortcut: 'CommandOrControl+Alt+Y' })
    registerCollectionCommand(config, 'editor', ['metadata', 'editors'], { keyboardShortcut: 'CommandOrControl+Alt+E', nodeType: 'person' })
    registerCollectionCommand(config, 'footnote', ['article', 'footnotes'], { automaticOrder: true, Command: InsertFootnoteCommand })
    registerCollectionCommand(config, 'group', ['metadata', 'groups'], { keyboardShortcut: 'CommandOrControl+Alt+G' })
    registerCollectionCommand(config, 'keyword', ['metadata', 'keywords'], { keyboardShortcut: 'CommandOrControl+Alt+K' })
    registerCollectionCommand(config, 'organisation', ['metadata', 'organisations'], { keyboardShortcut: 'CommandOrControl+Alt+O' })
    registerCollectionCommand(config, 'subject', ['metadata', 'subjects'])
    config.addCommand('insert-reference', AddEntityCommand, {
      workflow: 'add-reference',
      commandGroup: 'add-entity',
      type: 'bibr'
    })
  }
}

// TODO: this is like an overkill, registering one collection item command
// for every collection type
// it would be better to have just one set of commands
// which is detecting the collection automagically
// The challenge will then be how to control certain things,
// such as disabling move up/down etc.
function registerCollectionCommand (config, itemType, collectionPath, options = {}) {
  let nodeType = options.nodeType || itemType
  let xpathSelector = collectionPath.join('.')
  let Command = options.Command || AddEntityCommand
  config.addCommand(`insert-${itemType}`, Command, {
    type: nodeType,
    collection: collectionPath,
    commandGroup: 'add-entity'
  })
  if (options.keyboardShortcut) {
    config.addKeyboardShortcut(options.keyboardShortcut, { command: `add-${itemType}` })
  }
  if (!config.automaticOrder) {
    config.addCommand(`move-up-${itemType}`, MoveCollectionItemCommand, {
      direction: 'up',
      commandGroup: 'collection',
      xpathSelector
    })
    config.addIcon(`move-up-${itemType}`, { 'fontawesome': 'fa-caret-square-o-up' })
    config.addLabel(`move-up-${itemType}`, {
      en: 'Move item up'
    })
    config.addKeyboardShortcut('CommandOrControl+Alt+Up', { command: `move-up-${itemType}` })
    config.addCommand(`move-down-${itemType}`, MoveCollectionItemCommand, {
      direction: 'down',
      commandGroup: 'collection',
      xpathSelector
    })
    config.addIcon(`move-down-${itemType}`, { 'fontawesome': 'fa-caret-square-o-down' })
    config.addLabel(`move-down-${itemType}`, {
      en: 'Move item down'
    })
    config.addKeyboardShortcut('CommandOrControl+Alt+Down', { command: `move-down-${itemType}` })
    config.addCommand(`remove-${itemType}`, RemoveCollectionItemCommand, {
      commandGroup: 'collection',
      xpathSelector
    })

    config.addIcon('remove', {'fontawesome': 'fa-trash'})
    config.addIcon('checked-item', { 'fontawesome': 'fa-check-square-o' })
    config.addIcon('unchecked-item', { 'fontawesome': 'fa-square-o' })
  }
  config.addIcon(`remove-${itemType}`, { 'fontawesome': 'fa-trash' })
  config.addLabel(`remove-${itemType}`, {
    en: 'Remove item'
  })
  config.addKeyboardShortcut('CommandOrControl+Alt+Delete', { command: `remove-${itemType}` })
}
<|endoftext|>


<commit_before>/* globals describe, it, expect, expectObservable, expectSubscriptions, cold, hot */
var Rx = require('../../dist/cjs/Rx');
var Observable = Rx.Observable;

describe('Observable.prototype.distinctUntilKeyChanged()', function () {
  it('should distinguish between values', function () {
    var values = {a: {val: 1}, b: {val: 2}};
    var e1 =   hot('--a--a--a--b--b--a--|', values);
    var e1subs =   '^                   !';
    var expected = '--a--------b-----a--|';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should distinguish between values and does not completes', function () {
    var values = {a: {val: 1}, b: {val: 2}};
    var e1 =   hot('--a--a--a--b--b--a-', values);
    var e1subs =   '^                  ';
    var expected = '--a--------b-----a-';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should distinguish between values with key', function () {
    var values = {a: {val: 1}, b: {valOther: 1}, c: {valOther: 3}, d: {val: 1}, e: {val: 5}};
    var e1 =   hot('--a--b--c--d--e--|', values);
    var e1subs =   '^                !';
    var expected = '--a--b-----d--e--|';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should not compare if source does not have element with key', function () {
    var values = {a: {valOther: 1}, b: {valOther: 1}, c: {valOther: 3}, d: {valOther: 1}, e: {valOther: 5}};
    var e1 =   hot('--a--b--c--d--e--|', values);
    var e1subs =   '^                !';
    var expected = '--a--------------|';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should not completes if source never completes', function () {
    var e1 =  cold('-');
    var e1subs =   '^';
    var expected = '-';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should not completes if source does not completes', function () {
    var e1 =   hot('-');
    var e1subs =   '^';
    var expected = '-';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should complete if source is empty', function () {
    var e1 =  cold('|');
    var e1subs =   '(^!)';
    var expected = '|';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should complete if source does not emit', function () {
    var e1 =   hot('------|');
    var e1subs =   '^     !';
    var expected = '------|';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should emit if source emits single element only', function () {
    var values = {a: {val: 1}};
    var e1 =   hot('--a--|', values);
    var e1subs =   '^    !';
    var expected = '--a--|';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should emit if source is scalar', function () {
    var values = {a: {val: 1}};
    var e1 = Observable.of(values.a);
    var expected = '(a|)';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
  });

  it('should raises error if source raises error', function () {
    var values = {a: {val: 1}};
    var e1 =   hot('--a--a--#', values);
    var e1subs =   '^       !';
    var expected = '--a-----#';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should raises error if source throws', function () {
    var e1 =  cold('#');
    var e1subs =   '(^!)';
    var expected = '#';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should not omit if source elements are all different', function () {
    var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
    var e1 =   hot('--a--b--c--d--e--|', values);
    var e1subs =   '^                !';
    var expected = '--a--b--c--d--e--|';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should allow unsubscribing early and explicitly', function () {
    var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
    var e1 =   hot('--a--b--b--d--a--e--|', values);
    var e1subs =   '^         !          ';
    var expected = '--a--b-----          ';
    var unsub =    '          !          ';

    var result = e1.distinctUntilKeyChanged('val');

    expectObservable(result, unsub).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should not break unsubscription chains when unsubscribed explicitly', function () {
    var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
    var e1 =   hot('--a--b--b--d--a--e--|', values);
    var e1subs =   '^         !          ';
    var expected = '--a--b-----          ';
    var unsub =    '          !          ';

    var result = e1
      .mergeMap(function (x) { return Observable.of(x); })
      .distinctUntilKeyChanged('val')
      .mergeMap(function (x) { return Observable.of(x); });

    expectObservable(result, unsub).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should emit once if source elements are all same', function () {
    var values = {a: {val: 1}};
    var e1 =   hot('--a--a--a--a--a--a--|', values);
    var e1subs =   '^                   !';
    var expected = '--a-----------------|';

    expectObservable(e1.distinctUntilChanged()).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should emit once if comparer returns true always regardless of source emits', function () {
    var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
    var e1 =   hot('--a--b--c--d--e--|', values);
    var e1subs =   '^                !';
    var expected = '--a--------------|';

    expectObservable(e1.distinctUntilKeyChanged('val', function () { return true; })).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should emit all if comparer returns false always regardless of source emits', function () {
    var values = {a: {val: 1}};
    var e1 =   hot('--a--a--a--a--a--a--|', values);
    var e1subs =   '^                   !';
    var expected = '--a--a--a--a--a--a--|';

    expectObservable(e1.distinctUntilKeyChanged('val', function () { return false; })).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should distinguish values by selector', function () {
    var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
    var e1 =   hot('--a--b--c--d--e--|', values);
    var e1subs =   '^                !';
    var expected = '--a-----c-----e--|';
    var selector = function (x, y) {
      return y % 2 === 0;
    };

    expectObservable(e1.distinctUntilKeyChanged('val', selector)).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should raises error when comparer throws', function () {
    var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
    var e1 =   hot('--a--b--c--d--e--|', values);
    var e1subs =   '^          !      ';
    var expected = '--a--b--c--#      ';
    var selector = function (x, y) {
      if (y === 4) {
        throw 'error';
      }
      return x === y;
    };

    expectObservable(e1.distinctUntilKeyChanged('val', selector)).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });
});<commit_msg>test(distinctUntilKeyChanged): fix a test case using the wrong operator<commit_after>/* globals describe, it, expect, expectObservable, expectSubscriptions, cold, hot */
var Rx = require('../../dist/cjs/Rx');
var Observable = Rx.Observable;

describe('Observable.prototype.distinctUntilKeyChanged()', function () {
  it('should distinguish between values', function () {
    var values = {a: {val: 1}, b: {val: 2}};
    var e1 =   hot('--a--a--a--b--b--a--|', values);
    var e1subs =   '^                   !';
    var expected = '--a--------b-----a--|';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should distinguish between values and does not completes', function () {
    var values = {a: {val: 1}, b: {val: 2}};
    var e1 =   hot('--a--a--a--b--b--a-', values);
    var e1subs =   '^                  ';
    var expected = '--a--------b-----a-';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should distinguish between values with key', function () {
    var values = {a: {val: 1}, b: {valOther: 1}, c: {valOther: 3}, d: {val: 1}, e: {val: 5}};
    var e1 =   hot('--a--b--c--d--e--|', values);
    var e1subs =   '^                !';
    var expected = '--a--b-----d--e--|';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should not compare if source does not have element with key', function () {
    var values = {a: {valOther: 1}, b: {valOther: 1}, c: {valOther: 3}, d: {valOther: 1}, e: {valOther: 5}};
    var e1 =   hot('--a--b--c--d--e--|', values);
    var e1subs =   '^                !';
    var expected = '--a--------------|';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should not completes if source never completes', function () {
    var e1 =  cold('-');
    var e1subs =   '^';
    var expected = '-';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should not completes if source does not completes', function () {
    var e1 =   hot('-');
    var e1subs =   '^';
    var expected = '-';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should complete if source is empty', function () {
    var e1 =  cold('|');
    var e1subs =   '(^!)';
    var expected = '|';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should complete if source does not emit', function () {
    var e1 =   hot('------|');
    var e1subs =   '^     !';
    var expected = '------|';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should emit if source emits single element only', function () {
    var values = {a: {val: 1}};
    var e1 =   hot('--a--|', values);
    var e1subs =   '^    !';
    var expected = '--a--|';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should emit if source is scalar', function () {
    var values = {a: {val: 1}};
    var e1 = Observable.of(values.a);
    var expected = '(a|)';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
  });

  it('should raises error if source raises error', function () {
    var values = {a: {val: 1}};
    var e1 =   hot('--a--a--#', values);
    var e1subs =   '^       !';
    var expected = '--a-----#';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should raises error if source throws', function () {
    var e1 =  cold('#');
    var e1subs =   '(^!)';
    var expected = '#';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should not omit if source elements are all different', function () {
    var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
    var e1 =   hot('--a--b--c--d--e--|', values);
    var e1subs =   '^                !';
    var expected = '--a--b--c--d--e--|';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should allow unsubscribing early and explicitly', function () {
    var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
    var e1 =   hot('--a--b--b--d--a--e--|', values);
    var e1subs =   '^         !          ';
    var expected = '--a--b-----          ';
    var unsub =    '          !          ';

    var result = e1.distinctUntilKeyChanged('val');

    expectObservable(result, unsub).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should not break unsubscription chains when unsubscribed explicitly', function () {
    var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
    var e1 =   hot('--a--b--b--d--a--e--|', values);
    var e1subs =   '^         !          ';
    var expected = '--a--b-----          ';
    var unsub =    '          !          ';

    var result = e1
      .mergeMap(function (x) { return Observable.of(x); })
      .distinctUntilKeyChanged('val')
      .mergeMap(function (x) { return Observable.of(x); });

    expectObservable(result, unsub).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should emit once if source elements are all same', function () {
    var values = {a: {val: 1}};
    var e1 =   hot('--a--a--a--a--a--a--|', values);
    var e1subs =   '^                   !';
    var expected = '--a-----------------|';

    expectObservable(e1.distinctUntilKeyChanged('val')).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should emit once if comparer returns true always regardless of source emits', function () {
    var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
    var e1 =   hot('--a--b--c--d--e--|', values);
    var e1subs =   '^                !';
    var expected = '--a--------------|';

    expectObservable(e1.distinctUntilKeyChanged('val', function () { return true; })).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should emit all if comparer returns false always regardless of source emits', function () {
    var values = {a: {val: 1}};
    var e1 =   hot('--a--a--a--a--a--a--|', values);
    var e1subs =   '^                   !';
    var expected = '--a--a--a--a--a--a--|';

    expectObservable(e1.distinctUntilKeyChanged('val', function () { return false; })).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should distinguish values by selector', function () {
    var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
    var e1 =   hot('--a--b--c--d--e--|', values);
    var e1subs =   '^                !';
    var expected = '--a-----c-----e--|';
    var selector = function (x, y) {
      return y % 2 === 0;
    };

    expectObservable(e1.distinctUntilKeyChanged('val', selector)).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });

  it('should raises error when comparer throws', function () {
    var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
    var e1 =   hot('--a--b--c--d--e--|', values);
    var e1subs =   '^          !      ';
    var expected = '--a--b--c--#      ';
    var selector = function (x, y) {
      if (y === 4) {
        throw 'error';
      }
      return x === y;
    };

    expectObservable(e1.distinctUntilKeyChanged('val', selector)).toBe(expected, values);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });
});<|endoftext|>


<commit_before>'use strict';

require('./app-settings.less');
const T = require('./translation.json');
const css = require('./css');
const R = require('react').createElement;

function textSetting(props, header, description, name) {
  const {settings, updateSettings} = props;
  return R('div', {className: 'AppSettingGroup'},
    R('div', {className: 'AppSettingHeader'}, header),
    R('div', {className: 'AppSettingDescription'}, description),
    R('input', {
      className: 'AppSettingInput',
      value: settings[name],
      type: 'text',
      onChange: event =>
        updateSettings({[name]: event.target.value})
    })
  );
}

function checkboxSetting(props, header, description, name) {
  const {settings, updateSettings} = props;
  return R('div', {className: 'AppSettingGroup'},
    R('div', {className: 'AppSettingHeader'}, header),
    // R('div', {className: 'AppSettingDescription'}, description),
    R('label', {className: 'AppSettingCheckboxLabel'},
      R('input', {
        className: 'AppSettingCheckbox',
        value: settings[name],
        type: 'checkbox',
        onChange: event =>
          updateSettings({[name]: event.target.value})
      }),
      description
    )
  );
}

function getSettings(props) {
  return [
    textSetting(
      props,
      T.SETTINGS_HEADER_FOLDER,
      T.SETTINGS_DESCRIPTION_FOLDER,
      'folder'
    ),
    checkboxSetting(
      props,
      'Replace',
      'Should it replace?',
      'replace'
    )
  ];
}

function AppSettings(props) {
  const className = css({
    AppSettings: true,
    hidden: !props.isVisible
  });
  return R('div', {className},
    R('div', {className: 'AppSettingsTitleArea'},
      R('span', {className: 'AppSettingsTitle'}, T.SETTINGS),
      R('button', {
        className: 'AppSettingsCloseButton',
        onClick: props.onHideSettings
      }, T.CLOSE)
    ),
    R('div', {className: 'AppSettingsContent'},
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props)
    )
  );
}

module.exports = AppSettings;
<commit_msg>Oops, use `checked` not `value`<commit_after>'use strict';

require('./app-settings.less');
const T = require('./translation.json');
const css = require('./css');
const R = require('react').createElement;

function textSetting(props, header, description, name) {
  const {settings, updateSettings} = props;
  return R('div', {className: 'AppSettingGroup'},
    R('div', {className: 'AppSettingHeader'}, header),
    R('div', {className: 'AppSettingDescription'}, description),
    R('input', {
      className: 'AppSettingInput',
      value: settings[name],
      type: 'text',
      onChange: event =>
        updateSettings({[name]: event.target.value})
    })
  );
}

function checkboxSetting(props, header, description, name) {
  const {settings, updateSettings} = props;
  return R('div', {className: 'AppSettingGroup'},
    R('div', {className: 'AppSettingHeader'}, header),
    // R('div', {className: 'AppSettingDescription'}, description),
    R('label', {className: 'AppSettingCheckboxLabel'},
      R('input', {
        className: 'AppSettingCheckbox',
        checked: settings[name],
        type: 'checkbox',
        onChange: event =>
          updateSettings({[name]: event.target.checked})
      }),
      description
    )
  );
}

function getSettings(props) {
  return [
    textSetting(
      props,
      T.SETTINGS_HEADER_FOLDER,
      T.SETTINGS_DESCRIPTION_FOLDER,
      'folder'
    ),
    checkboxSetting(
      props,
      'Replace',
      'Should it replace?',
      'replace'
    )
  ];
}

function AppSettings(props) {
  const className = css({
    AppSettings: true,
    hidden: !props.isVisible
  });
  return R('div', {className},
    R('div', {className: 'AppSettingsTitleArea'},
      R('span', {className: 'AppSettingsTitle'}, T.SETTINGS),
      R('button', {
        className: 'AppSettingsCloseButton',
        onClick: props.onHideSettings
      }, T.CLOSE)
    ),
    R('div', {className: 'AppSettingsContent'},
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props),
      getSettings(props)
    )
  );
}

module.exports = AppSettings;
<|endoftext|>


<commit_before>'use strict';

var SyncSpec = require('./../lib/bspec').SyncSpec;
var util = require('util');

// can be implemented with bspec- factory functions

// is expired
function OrderExpiredSpec() { }
util.inherits(OrderExpiredSpec, SyncSpec);
OrderExpiredSpec.prototype.isSatisfiedBy = function isSatisfiedBy(order) {
  return (Date.now() > order.date);
};

// is valid
function OrderValidSpec() { }
util.inherits(OrderValidSpec, SyncSpec);
OrderValidSpec.prototype.isSatisfiedBy = function isSatisfiedBy(order) {
  return order && order.hasOwnProperty('number') && order.hasOwnProperty('date');
};

// is processed
var processed = {};
function OrderProcessed() { }
util.inherits(OrderProcessed, SyncSpec);
OrderProcessed.prototype.isSatisfiedBy = function isSatisfiedBy(order) {
  return !!processed[order.number];
};

var isExpired = new OrderExpiredSpec();
var isValid = new OrderValidSpec();
var isProcessed = new OrderProcessed();

var spec = isValid.and(isExpired.not()).and(isProcessed.not());

var order = { number: 'Z10',  date: new Date('2014-05-06') };

if(spec.isSatisfiedBy(order)) {
  // process order
} else {

}

// inspect composite specification
console.log(spec.explain()); // ((ValidOrderSpec AND (NOT OverDueOrderSpec)) AND (NOT OrderProcessed))
<commit_msg>fixed example<commit_after>'use strict';

var SyncSpec = require('./../lib/bspec').SyncSpec;
var util = require('util');

// can be implemented with bspec- factory functions

// is expired
function OrderExpiredSpec() { }

util.inherits(OrderExpiredSpec, SyncSpec);

OrderExpiredSpec.prototype.isSatisfiedBy = function isSatisfiedBy(order) {
  return (Date.now() > order.date);
};

// is valid
function OrderValidSpec() { }

util.inherits(OrderValidSpec, SyncSpec);

OrderValidSpec.prototype.isSatisfiedBy = function isSatisfiedBy(order) {
  return order && order.hasOwnProperty('number') && order.hasOwnProperty('date');
};

// is processed
var processed = {};

function OrderProcessed() { }

util.inherits(OrderProcessed, SyncSpec);

OrderProcessed.prototype.isSatisfiedBy = function isSatisfiedBy(order) {
  return !!processed[order.number];
};

var isExpired = new OrderExpiredSpec();
var isValid = new OrderValidSpec();
var isProcessed = new OrderProcessed();

var spec = isValid.and(isExpired.not()).and(isProcessed.not());

var order = { number: 'Z10',  date: new Date('2014-05-06') };

if(spec.isSatisfiedBy(order)) {
  // process order
} else {

}

// inspect composite specification
console.log(spec.explain()); // ((ValidOrderSpec AND (NOT OverDueOrderSpec)) AND (NOT OrderProcessed))
<|endoftext|>


<commit_before>/*** @used { core.RAIIEventEmitter } **/

exports.capabilities = {}

var registerGenericListener = function(target) {
	var prefix = '__nativeEventHandler_'
	target.onListener('',
		function(name) {
			log('registering generic event', name)
			var pname = prefix + name
			var callback = target[pname] = function() {
				COPY_ARGS(args, 0)
				target.emitWithArgs(name, args)
			}
			//target.dom.addEventListener(name, callback)
		},
		function(name) {
			log('removing generic event', name)
			var pname = prefix + name
			//target.dom.removeEventListener(name, target[pname])
		}
	)
}

var updatedItems = new Set()
var rootItem = null

var PureText = function(data) {
	this.layout(data)
}

PureText.prototype.constructor = PureText
PureText.prototype.layout = function(data) {
	log('laying out "' + data.text + '"')
}

var PureImage = function(src) {
	this.load(src)
}

PureImage.prototype.constructor = PureImage
PureImage.prototype.load = function(src) {
	log('loading image from ' + src)
}

var Element = function(context, tag) {
	_globals.core.RAIIEventEmitter.apply(this)
	this._context = context
	this._styles = {}
	this._pure = null
	this.children = []
	registerGenericListener(this)
}

Element.prototype = Object.create(_globals.core.RAIIEventEmitter.prototype)
Element.prototype.constructor = Element

Element.prototype.addClass = function(cls) { }
Element.prototype.setHtml = function(cls) { }
Element.prototype.style = function(name, style) {
	if (style !== undefined) {
		if (style !== '') //fixme: replace it with explicit 'undefined' syntax
			this._styles[name] = style
		else
			delete this._styles[name]
	} else if (name instanceof Object) { //style({ }) assignment
		for(var k in name) {
			var value = name[k]
			if (value !== '') //fixme: replace it with explicit 'undefined' syntax
				this._styles[k] = value
			else
				delete this._styles[k]
		}
	}
	else
		return this._styles[name]
}
Element.prototype.updateStyle = function() { }

Element.prototype.append = function(child) {
	if (child._parent !== undefined)
		throw new Error('double append on element')
	child._parent = this
	this.children.push(child)
}

Element.prototype.remove = function() {
	var parent = this._parent
	if (parent !== undefined) {
		var idx = parent.children.indexOf(this)
		if (idx < 0)
			throw new Error('remove(): no child in parent children array')
		parent.children.splice(idx, 1)
		this._parent = undefined
	} else
		throw new Error('remove() called without adding to parent')
}

exports.init = function(ctx) {
	rootItem = ctx.element = new Element(ctx, ctx.getTag())
}

exports.run = function(ctx) {
	ctx._run()
}

exports.createElement = function(ctx, tag) {
	return new Element(ctx, tag)
}

exports.initImage = function(image) {
	image._pure = new PureImage(image.source)
}

exports.loadImage = function(image) {
	image._pure.load(image.source)
}

exports.layoutText = function(text) {
	text._pure = new PureText(text)
}

exports.requestAnimationFrame = function(callback) {
	return setTimeout(callback, 0)
}

exports.cancelAnimationFrame = function (timer) {
	clearTimeout(timer)
}
<commit_msg>mark item updated if one of the important styles have changed<commit_after>/*** @used { core.RAIIEventEmitter } **/

exports.capabilities = {}

var registerGenericListener = function(target) {
	var prefix = '__nativeEventHandler_'
	target.onListener('',
		function(name) {
			log('registering generic event', name)
			var pname = prefix + name
			var callback = target[pname] = function() {
				COPY_ARGS(args, 0)
				target.emitWithArgs(name, args)
			}
			//target.dom.addEventListener(name, callback)
		},
		function(name) {
			log('removing generic event', name)
			var pname = prefix + name
			//target.dom.removeEventListener(name, target[pname])
		}
	)
}

var updatedItems = new Set()
var rootItem = null

var PureText = function(data) {
	this.layout(data)
}

PureText.prototype.constructor = PureText
PureText.prototype.layout = function(data) {
	log('laying out "' + data.text + '"')
}

var PureImage = function(src) {
	this.load(src)
}

PureImage.prototype.constructor = PureImage
PureImage.prototype.load = function(src) {
	log('loading image from ' + src)
}

var Element = function(context, tag) {
	_globals.core.RAIIEventEmitter.apply(this)
	this._context = context
	this._styles = {}
	this._pure = null
	this.children = []
	registerGenericListener(this)
}

Element.prototype = Object.create(_globals.core.RAIIEventEmitter.prototype)
Element.prototype.constructor = Element

Element.prototype.addClass = function(cls) { }
Element.prototype.setHtml = function(cls) { }

var importantStyles = new Set([
	'left', 'top', 'width', 'height', 'visibility',
	'background-color', 'border-radius',
	'color', 'font-size', 'text-align'
])

Element.prototype._onUpdate = function(name) {
	if (importantStyles.has(name))
		updatedItems.add(this)
//	else
//		log('unhandled style ' + name)
}

Element.prototype.style = function(name, style) {
	if (style !== undefined) {
		this._onUpdate(name)
		if (style !== '') //fixme: replace it with explicit 'undefined' syntax
			this._styles[name] = style
		else
			delete this._styles[name]
	} else if (name instanceof Object) { //style({ }) assignment
		for(var k in name) {
			this._onUpdate(k)
			var value = name[k]
			if (value !== '') //fixme: replace it with explicit 'undefined' syntax
				this._styles[k] = value
			else
				delete this._styles[k]
		}
	}
	else
		return this._styles[name]
}
Element.prototype.updateStyle = function() { }

Element.prototype.append = function(child) {
	if (child._parent !== undefined)
		throw new Error('double append on element')
	child._parent = this
	this.children.push(child)
}

Element.prototype.remove = function() {
	var parent = this._parent
	if (parent !== undefined) {
		var idx = parent.children.indexOf(this)
		if (idx < 0)
			throw new Error('remove(): no child in parent children array')
		parent.children.splice(idx, 1)
		this._parent = undefined
	} else
		throw new Error('remove() called without adding to parent')
}

exports.init = function(ctx) {
	rootItem = ctx.element = new Element(ctx, ctx.getTag())
}

exports.run = function(ctx) {
	ctx._run()
}

exports.createElement = function(ctx, tag) {
	return new Element(ctx, tag)
}

exports.initImage = function(image) {
	image._pure = new PureImage(image.source)
}

exports.loadImage = function(image) {
	image._pure.load(image.source)
}

exports.layoutText = function(text) {
	text._pure = new PureText(text)
}

exports.requestAnimationFrame = function(callback) {
	return setTimeout(callback, 0)
}

exports.cancelAnimationFrame = function (timer) {
	clearTimeout(timer)
}
<|endoftext|>


<commit_before>/*
 * A bot that shows how to access and search the logs of a specific channel.
 * Specifically, it returns the last message from a given user in the last
 * 100 messages.
 */

var Discord = require( "discord.js" );
var myBot = new Discord.Client();

myBot.login( "hello@example.com", "password1" );

myBot.on( "message", function( message ) {
	// React to all messages starting with "$query".
	if ( message.content.startsWith( "$query" ) ) {
    // Obtain the channel for which logs should be accessed.
    var channel = message.channel;

    // Find all the arguments to the command.
    var arguments = message.content.split(" ");

    // Get the first argument specifically, as it contains the username
    // to be queried for.
    var username = arguments[1];

    // Exit the event handler unless the user exists.
    if( !username ) {
      myBot.sendMessage(channel, "That user doesn't exist!");
      return;
    }

    // The getChannelLogs() function takes the channel that should be accessed,
    // the amount of messages to query and a callback as its arguments.
    myBot.getChannelLogs( channel, 100, function( messageList ) {
      // filter() takes three arguments, the key to be filtered for (in this
      // case the username, so "username"), the value to look for, and whether
      // only the first finding should be returned (true) or a list of all
      // findings (false).
      var message = messageList.filter( "username", username, true );

      // Only continue if the message has been found
      if( message ) {
        myBot.sendMessage(channel, "The last message from user " + username +
            " is: \"" + message.content + "\".").
      } else {
        myBot.sendMessage("That user has not sent a message " +
            "for the last 100 messages!")
      }
    } );
	}
} );
<commit_msg>Fix query.js to properly deal with users with spaces in their name.<commit_after>/*
 * A bot that shows how to access and search the logs of a specific channel.
 * Specifically, it returns the last message from a given user in the last
 * 100 messages.
 */

var Discord = require( "discord.js" );
var myBot = new Discord.Client();

myBot.login( "hello@example.com", "password1" );

myBot.on( "message", function( message ) {
	// React to all messages starting with "$query".
	if ( message.content.startsWith( "$query" ) ) {
    // Obtain the channel for which logs should be accessed.
    var channel = message.channel;

    // Find all the arguments to the command.
    var arguments = message.content.split( " " );

    // Get the first argument specifically, as it contains the username
    // to be queried for.
    var username = arguments.slice( 1 ).join( " " );

    // Exit the event handler unless the user exists.
    if( !username ) {
      myBot.sendMessage( channel, "That user doesn't exist!" );
      return;
    }

    // The getChannelLogs() function takes the channel that should be accessed,
    // the amount of messages to query and a callback as its arguments.
    myBot.getChannelLogs( channel, 100, function( messageList ) {
      // filter() takes three arguments, the key to be filtered for (in this
      // case the username, so "username"), the value to look for, and whether
      // only the first finding should be returned (true) or a list of all
      // findings (false).
      var message = messageList.filter( "username", username, true );

      // Only continue if the message has been found
      if( message ) {
        myBot.sendMessage( channel, "The last message from user " + username +
            " is: \"" + message.content + "\"." ).
      } else {
        myBot.sendMessage( "That user has not sent a message " +
            "for the last 100 messages!" )
      }
    } );
	}
} );
<|endoftext|>