<commit_before>/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/**
 * Cross signing methods
 * @module crypto/CrossSigning
 */

import {pkSign, pkVerify} from './olmlib';
import {EventEmitter} from 'events';
import logger from '../logger';

function publicKeyFromKeyInfo(keyInfo) {
    return Object.entries(keyInfo.keys)[0];
}

export class CrossSigningInfo extends EventEmitter {
    /**
     * Information about a user's cross-signing keys
     *
     * @class
     *
     * @param {string} userId the user that the information is about
     * @param {object} callbacks Callbacks used to interact with the app
     *     Requires getCrossSigningKey and saveCrossSigningKeys
     */
    constructor(userId, callbacks) {
        super();

        // you can't change the userId
        Object.defineProperty(this, 'userId', {
            enumerable: true,
            value: userId,
        });
        this._callbacks = callbacks || {};
        this.keys = {};
        this.firstUse = true;
    }

    /**
     * Calls the app callback to ask for a private key
     * @param {string} type The key type ("master", "self_signing", or "user_signing")
     * @param {Uint8Array} expectedPubkey The matching public key or undefined to use
     *     the stored public key for the given key type.
     */
    async getCrossSigningKey(type, expectedPubkey) {
        if (!this._callbacks.getCrossSigningKey) {
            throw new Error("No getCrossSigningKey callback supplied");
        }

        if (expectedPubkey === undefined) {
            expectedPubkey = this.getId(type);
        }

        const privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey);
        if (!privkey) {
            throw new Error(
                "getCrossSigningKey callback for  " + type + " returned falsey",
            );
        }
        const signing = new global.Olm.PkSigning();
        const gotPubkey = signing.init_with_seed(privkey);
        if (gotPubkey !== expectedPubkey) {
            signing.free();
            throw new Error(
                "Key type " + type + " from getCrossSigningKey callback did not match",
            );
        } else {
            return [gotPubkey, signing];
        }
    }

    static fromStorage(obj, userId) {
        const res = new CrossSigningInfo(userId);
        for (const prop in obj) {
            if (obj.hasOwnProperty(prop)) {
                res[prop] = obj[prop];
            }
        }
        return res;
    }

    toStorage() {
        return {
            keys: this.keys,
            firstUse: this.firstUse,
        };
    }

    /**
     * Get the ID used to identify the user
     *
     * @param {string} type The type of key to get the ID of.  One of "master",
     * "self_signing", or "user_signing".  Defaults to "master".
     *
     * @return {string} the ID
     */
    getId(type) {
        type = type || "master";
        if (!this.keys[type]) return null;
        const keyInfo = this.keys[type];
        return publicKeyFromKeyInfo(keyInfo)[1];
    }

    async resetKeys(level) {
        if (!this._callbacks.saveCrossSigningKeys) {
            throw new Error("No saveCrossSigningKeys callback supplied");
        }

        // If we're resetting the master key, we reset all keys
        if (
            level === undefined ||
            level & CrossSigningLevel.MASTER ||
            !this.keys.master
        ) {
            level = (
                CrossSigningLevel.MASTER |
                CrossSigningLevel.USER_SIGNING |
                CrossSigningLevel.SELF_SIGNING
            );
        } else if (level === 0) {
            return;
        }

        const privateKeys = {};
        const keys = {};
        let masterSigning;
        let masterPub;

        try {
            if (level & CrossSigningLevel.MASTER) {
                masterSigning = new global.Olm.PkSigning();
                privateKeys.master = masterSigning.generate_seed();
                masterPub = masterSigning.init_with_seed(privateKeys.master);
                keys.master = {
                    user_id: this.userId,
                    usage: ['master'],
                    keys: {
                        ['ed25519:' + masterPub]: masterPub,
                    },
                };
            } else {
                [masterPub, masterSigning] = await this.getCrossSigningyKey("master");
            }

            if (level & CrossSigningLevel.SELF_SIGNING) {
                const sskSigning = new global.Olm.PkSigning();
                try {
                    privateKeys.self_signing = sskSigning.generate_seed();
                    const sskPub = sskSigning.init_with_seed(privateKeys.self_signing);
                    keys.self_signing = {
                        user_id: this.userId,
                        usage: ['self_signing'],
                        keys: {
                            ['ed25519:' + sskPub]: sskPub,
                        },
                    };
                    pkSign(keys.self_signing, masterSigning, this.userId, masterPub);
                } finally {
                    sskSigning.free();
                }
            }

            if (level & CrossSigningLevel.USER_SIGNING) {
                const uskSigning = new global.Olm.PkSigning();
                try {
                    privateKeys.user_signing = uskSigning.generate_seed();
                    const uskPub = uskSigning.init_with_seed(privateKeys.user_signing);
                    keys.user_signing = {
                        user_id: this.userId,
                        usage: ['user_signing'],
                        keys: {
                            ['ed25519:' + uskPub]: uskPub,
                        },
                    };
                    pkSign(keys.user_signing, masterSigning, this.userId, masterPub);
                } finally {
                    uskSigning.free();
                }
            }

            Object.assign(this.keys, keys);
            this._callbacks.saveCrossSigningKeys(privateKeys);
        } finally {
            if (masterSigning) {
                masterSigning.free();
            }
        }
    }

    setKeys(keys) {
        const signingKeys = {};
        if (keys.master) {
            if (keys.master.user_id !== this.userId) {
                const error = "Mismatched user ID " + keys.master.user_id +
                      " in master key from " + this.userId;
                logger.error(error);
                throw new Error(error);
            }
            if (!this.keys.master) {
                // this is the first key we've seen, so first-use is true
                this.firstUse = true;
            } else if (publicKeyFromKeyInfo(keys.master)[1] !== this.getId()) {
                // this is a different key, so first-use is false
                this.firstUse = false;
            } // otherwise, same key, so no change
            signingKeys.master = keys.master;
        } else if (this.keys.master) {
            signingKeys.master = this.keys.master;
        } else {
            throw new Error("Tried to set cross-signing keys without a master key");
        }
        const masterKey = publicKeyFromKeyInfo(signingKeys.master)[1];

        // verify signatures
        if (keys.user_signing) {
            if (keys.user_signing.user_id !== this.userId) {
                const error = "Mismatched user ID " + keys.master.user_id +
                      " in user_signing key from " + this.userId;
                logger.error(error);
                throw new Error(error);
            }
            try {
                pkVerify(keys.user_signing, masterKey, this.userId);
            } catch (e) {
                logger.error("invalid signature on user-signing key");
                // FIXME: what do we want to do here?
                throw e;
            }
        }
        if (keys.self_signing) {
            if (keys.self_signing.user_id !== this.userId) {
                const error = "Mismatched user ID " + keys.master.user_id +
                      " in self_signing key from " + this.userId;
                logger.error(error);
                throw new Error(error);
            }
            try {
                pkVerify(keys.self_signing, masterKey, this.userId);
            } catch (e) {
                logger.error("invalid signature on self-signing key");
                // FIXME: what do we want to do here?
                throw e;
            }
        }

        // if everything checks out, then save the keys
        if (keys.master) {
            this.keys.master = keys.master;
            // if the master key is set, then the old self-signing and
            // user-signing keys are obsolete
            delete this.keys.self_signing;
            delete this.keys.user_signing;
        }
        if (keys.self_signing) {
            this.keys.self_signing = keys.self_signing;
        }
        if (keys.user_signing) {
            this.keys.user_signing = keys.user_signing;
        }
    }

    async signObject(data, type) {
        if (!this.keys[type]) {
            throw new Error(
                "Attempted to sign with " + type + " key but no such key present",
            );
        }
        const [pubkey, signing] = await this.getCrossSigningKey(type);
        try {
            pkSign(data, signing, this.userId, pubkey);
            return data;
        } finally {
            signing.free();
        }
    }

    async signUser(key) {
        if (!this.keys.user_signing) {
            return;
        }
        return this.signObject(key.keys.master, "user_signing");
    }

    async signDevice(userId, device) {
        if (userId !== this.userId) {
            throw new Error(
                `Trying to sign ${userId}'s device; can only sign our own device`,
            );
        }
        if (!this.keys.self_signing) {
            return;
        }
        return this.signObject(
            {
                algorithms: device.algorithms,
                keys: device.keys,
                device_id: device.deviceId,
                user_id: userId,
            }, "self_signing",
        );
    }

    /**
     * Check whether a given user is trusted.
     *
     * @param {CrossSigningInfo} userCrossSigning Cross signing info for user
     *
     * @returns {UserTrustLevel}
     */
    checkUserTrust(userCrossSigning) {
        // if we're checking our own key, then it's trusted if the master key
        // and self-signing key match
        if (this.userId === userCrossSigning.userId
            && this.getId() && this.getId() === userCrossSigning.getId()
            && this.getId("self_signing")
            && this.getId("self_signing") === userCrossSigning.getId("self_signing")
        ) {
            return new UserTrustLevel(true, this.firstUse);
        }

        if (!this.keys.user_signing) {
            // If there's no user signing key, they can't possibly be verified.
            // They may be TOFU trusted though.
            return new UserTrustLevel(false, userCrossSigning.firstUse);
        }

        let userTrusted;
        const userMaster = userCrossSigning.keys.master;
        const uskId = this.getId('user_signing');
        try {
            pkVerify(userMaster, uskId, this.userId);
            userTrusted = true;
        } catch (e) {
            userTrusted = false;
        }
        return new UserTrustLevel(userTrusted, userCrossSigning.firstUse);
    }

    /**
     * Check whether a given device is trusted.
     *
     * @param {CrossSigningInfo} userCrossSigning Cross signing info for user
     * @param {module:crypto/deviceinfo} device The device to check
     * @param {bool} localTrust Whether the device is trusted locally
     *
     * @returns {DeviceTrustLevel}
     */
    checkDeviceTrust(userCrossSigning, device, localTrust) {
        const userTrust = this.checkUserTrust(userCrossSigning);

        const userSSK = userCrossSigning.keys.self_signing;
        if (!userSSK) {
            // if the user has no self-signing key then we cannot make any
            // trust assertions about this device from cross-signing
            return new DeviceTrustLevel(false, false, localTrust);
        }

        const deviceObj = deviceToObject(device, userCrossSigning.userId);
        try {
            // if we can verify the user's SSK from their master key...
            pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId);
            // ...and this device's key from their SSK...
            pkVerify(
                deviceObj, publicKeyFromKeyInfo(userSSK)[1], userCrossSigning.userId,
            );
            // ...then we trust this device as much as far as we trust the user
            return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust);
        } catch (e) {
            return new DeviceTrustLevel(false, false, localTrust);
        }
    }
}

function deviceToObject(device, userId) {
    return {
        algorithms: device.algorithms,
        keys: device.keys,
        device_id: device.deviceId,
        user_id: userId,
        signatures: device.signatures,
    };
}

export const CrossSigningLevel = {
    MASTER: 4,
    USER_SIGNING: 2,
    SELF_SIGNING: 1,
};

/**
 * Represents the ways in which we trust a user
 */
export class UserTrustLevel {
    constructor(crossSigningVerified, tofu) {
        this._crossSigningVerified = crossSigningVerified;
        this._tofu = tofu;
    }

    /**
     * @returns {bool} true if this user is verified via any means
     */
    isVerified() {
        return this.isCrossSigningVerified();
    }

    /**
     * @returns {bool} true if this user is verified via cross signing
     */
    isCrossSigningVerified() {
        return this._crossSigningVerified;
    }

    /**
     * @returns {bool} true if this user's key is trusted on first use
     */
    isTofu() {
        return this._tofu;
    }
}

/**
 * Represents the ways in which we trust a device
 */
export class DeviceTrustLevel {
    constructor(crossSigningVerified, tofu, localVerified) {
        this._crossSigningVerified = crossSigningVerified;
        this._tofu = tofu;
        this._localVerified = localVerified;
    }

    static fromUserTrustLevel(userTrustLevel, localVerified) {
        return new DeviceTrustLevel(
            userTrustLevel._crossSigningVerified,
            userTrustLevel._tofu,
            localVerified,
        );
    }

    /**
     * @returns {bool} true if this device is verified via any means
     */
    isVerified() {
        return this.isCrossSigningVerified() || this.isLocallyVerified();
    }

    /**
     * @returns {bool} true if this device is verified via cross signing
     */
    isCrossSigningVerified() {
        return this._crossSigningVerified;
    }

    /**
     * @returns {bool} true if this device is verified locally
     */
    isLocallyVerified() {
        return this._localVerified;
    }

    /**
     * @returns {bool} true if this user's key is trusted on first use
     */
    isTofu() {
        return this._tofu;
    }
}
<commit_msg>update jsdoc<commit_after>/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/**
 * Cross signing methods
 * @module crypto/CrossSigning
 */

import {pkSign, pkVerify} from './olmlib';
import {EventEmitter} from 'events';
import logger from '../logger';

function publicKeyFromKeyInfo(keyInfo) {
    return Object.entries(keyInfo.keys)[0];
}

export class CrossSigningInfo extends EventEmitter {
    /**
     * Information about a user's cross-signing keys
     *
     * @class
     *
     * @param {string} userId the user that the information is about
     * @param {object} callbacks Callbacks used to interact with the app
     *     Requires getCrossSigningKey and saveCrossSigningKeys
     */
    constructor(userId, callbacks) {
        super();

        // you can't change the userId
        Object.defineProperty(this, 'userId', {
            enumerable: true,
            value: userId,
        });
        this._callbacks = callbacks || {};
        this.keys = {};
        this.firstUse = true;
    }

    /**
     * Calls the app callback to ask for a private key
     * @param {string} type The key type ("master", "self_signing", or "user_signing")
     * @param {Uint8Array} expectedPubkey The matching public key or undefined to use
     *     the stored public key for the given key type.
     */
    async getCrossSigningKey(type, expectedPubkey) {
        if (!this._callbacks.getCrossSigningKey) {
            throw new Error("No getCrossSigningKey callback supplied");
        }

        if (expectedPubkey === undefined) {
            expectedPubkey = this.getId(type);
        }

        const privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey);
        if (!privkey) {
            throw new Error(
                "getCrossSigningKey callback for  " + type + " returned falsey",
            );
        }
        const signing = new global.Olm.PkSigning();
        const gotPubkey = signing.init_with_seed(privkey);
        if (gotPubkey !== expectedPubkey) {
            signing.free();
            throw new Error(
                "Key type " + type + " from getCrossSigningKey callback did not match",
            );
        } else {
            return [gotPubkey, signing];
        }
    }

    static fromStorage(obj, userId) {
        const res = new CrossSigningInfo(userId);
        for (const prop in obj) {
            if (obj.hasOwnProperty(prop)) {
                res[prop] = obj[prop];
            }
        }
        return res;
    }

    toStorage() {
        return {
            keys: this.keys,
            firstUse: this.firstUse,
        };
    }

    /**
     * Get the ID used to identify the user
     *
     * @param {string} type The type of key to get the ID of.  One of "master",
     * "self_signing", or "user_signing".  Defaults to "master".
     *
     * @return {string} the ID
     */
    getId(type) {
        type = type || "master";
        if (!this.keys[type]) return null;
        const keyInfo = this.keys[type];
        return publicKeyFromKeyInfo(keyInfo)[1];
    }

    async resetKeys(level) {
        if (!this._callbacks.saveCrossSigningKeys) {
            throw new Error("No saveCrossSigningKeys callback supplied");
        }

        // If we're resetting the master key, we reset all keys
        if (
            level === undefined ||
            level & CrossSigningLevel.MASTER ||
            !this.keys.master
        ) {
            level = (
                CrossSigningLevel.MASTER |
                CrossSigningLevel.USER_SIGNING |
                CrossSigningLevel.SELF_SIGNING
            );
        } else if (level === 0) {
            return;
        }

        const privateKeys = {};
        const keys = {};
        let masterSigning;
        let masterPub;

        try {
            if (level & CrossSigningLevel.MASTER) {
                masterSigning = new global.Olm.PkSigning();
                privateKeys.master = masterSigning.generate_seed();
                masterPub = masterSigning.init_with_seed(privateKeys.master);
                keys.master = {
                    user_id: this.userId,
                    usage: ['master'],
                    keys: {
                        ['ed25519:' + masterPub]: masterPub,
                    },
                };
            } else {
                [masterPub, masterSigning] = await this.getCrossSigningyKey("master");
            }

            if (level & CrossSigningLevel.SELF_SIGNING) {
                const sskSigning = new global.Olm.PkSigning();
                try {
                    privateKeys.self_signing = sskSigning.generate_seed();
                    const sskPub = sskSigning.init_with_seed(privateKeys.self_signing);
                    keys.self_signing = {
                        user_id: this.userId,
                        usage: ['self_signing'],
                        keys: {
                            ['ed25519:' + sskPub]: sskPub,
                        },
                    };
                    pkSign(keys.self_signing, masterSigning, this.userId, masterPub);
                } finally {
                    sskSigning.free();
                }
            }

            if (level & CrossSigningLevel.USER_SIGNING) {
                const uskSigning = new global.Olm.PkSigning();
                try {
                    privateKeys.user_signing = uskSigning.generate_seed();
                    const uskPub = uskSigning.init_with_seed(privateKeys.user_signing);
                    keys.user_signing = {
                        user_id: this.userId,
                        usage: ['user_signing'],
                        keys: {
                            ['ed25519:' + uskPub]: uskPub,
                        },
                    };
                    pkSign(keys.user_signing, masterSigning, this.userId, masterPub);
                } finally {
                    uskSigning.free();
                }
            }

            Object.assign(this.keys, keys);
            this._callbacks.saveCrossSigningKeys(privateKeys);
        } finally {
            if (masterSigning) {
                masterSigning.free();
            }
        }
    }

    setKeys(keys) {
        const signingKeys = {};
        if (keys.master) {
            if (keys.master.user_id !== this.userId) {
                const error = "Mismatched user ID " + keys.master.user_id +
                      " in master key from " + this.userId;
                logger.error(error);
                throw new Error(error);
            }
            if (!this.keys.master) {
                // this is the first key we've seen, so first-use is true
                this.firstUse = true;
            } else if (publicKeyFromKeyInfo(keys.master)[1] !== this.getId()) {
                // this is a different key, so first-use is false
                this.firstUse = false;
            } // otherwise, same key, so no change
            signingKeys.master = keys.master;
        } else if (this.keys.master) {
            signingKeys.master = this.keys.master;
        } else {
            throw new Error("Tried to set cross-signing keys without a master key");
        }
        const masterKey = publicKeyFromKeyInfo(signingKeys.master)[1];

        // verify signatures
        if (keys.user_signing) {
            if (keys.user_signing.user_id !== this.userId) {
                const error = "Mismatched user ID " + keys.master.user_id +
                      " in user_signing key from " + this.userId;
                logger.error(error);
                throw new Error(error);
            }
            try {
                pkVerify(keys.user_signing, masterKey, this.userId);
            } catch (e) {
                logger.error("invalid signature on user-signing key");
                // FIXME: what do we want to do here?
                throw e;
            }
        }
        if (keys.self_signing) {
            if (keys.self_signing.user_id !== this.userId) {
                const error = "Mismatched user ID " + keys.master.user_id +
                      " in self_signing key from " + this.userId;
                logger.error(error);
                throw new Error(error);
            }
            try {
                pkVerify(keys.self_signing, masterKey, this.userId);
            } catch (e) {
                logger.error("invalid signature on self-signing key");
                // FIXME: what do we want to do here?
                throw e;
            }
        }

        // if everything checks out, then save the keys
        if (keys.master) {
            this.keys.master = keys.master;
            // if the master key is set, then the old self-signing and
            // user-signing keys are obsolete
            delete this.keys.self_signing;
            delete this.keys.user_signing;
        }
        if (keys.self_signing) {
            this.keys.self_signing = keys.self_signing;
        }
        if (keys.user_signing) {
            this.keys.user_signing = keys.user_signing;
        }
    }

    async signObject(data, type) {
        if (!this.keys[type]) {
            throw new Error(
                "Attempted to sign with " + type + " key but no such key present",
            );
        }
        const [pubkey, signing] = await this.getCrossSigningKey(type);
        try {
            pkSign(data, signing, this.userId, pubkey);
            return data;
        } finally {
            signing.free();
        }
    }

    async signUser(key) {
        if (!this.keys.user_signing) {
            return;
        }
        return this.signObject(key.keys.master, "user_signing");
    }

    async signDevice(userId, device) {
        if (userId !== this.userId) {
            throw new Error(
                `Trying to sign ${userId}'s device; can only sign our own device`,
            );
        }
        if (!this.keys.self_signing) {
            return;
        }
        return this.signObject(
            {
                algorithms: device.algorithms,
                keys: device.keys,
                device_id: device.deviceId,
                user_id: userId,
            }, "self_signing",
        );
    }

    /**
     * Check whether a given user is trusted.
     *
     * @param {CrossSigningInfo} userCrossSigning Cross signing info for user
     *
     * @returns {UserTrustLevel}
     */
    checkUserTrust(userCrossSigning) {
        // if we're checking our own key, then it's trusted if the master key
        // and self-signing key match
        if (this.userId === userCrossSigning.userId
            && this.getId() && this.getId() === userCrossSigning.getId()
            && this.getId("self_signing")
            && this.getId("self_signing") === userCrossSigning.getId("self_signing")
        ) {
            return new UserTrustLevel(true, this.firstUse);
        }

        if (!this.keys.user_signing) {
            // If there's no user signing key, they can't possibly be verified.
            // They may be TOFU trusted though.
            return new UserTrustLevel(false, userCrossSigning.firstUse);
        }

        let userTrusted;
        const userMaster = userCrossSigning.keys.master;
        const uskId = this.getId('user_signing');
        try {
            pkVerify(userMaster, uskId, this.userId);
            userTrusted = true;
        } catch (e) {
            userTrusted = false;
        }
        return new UserTrustLevel(userTrusted, userCrossSigning.firstUse);
    }

    /**
     * Check whether a given device is trusted.
     *
     * @param {CrossSigningInfo} userCrossSigning Cross signing info for user
     * @param {module:crypto/deviceinfo} device The device to check
     * @param {bool} localTrust Whether the device is trusted locally
     *
     * @returns {DeviceTrustLevel}
     */
    checkDeviceTrust(userCrossSigning, device, localTrust) {
        const userTrust = this.checkUserTrust(userCrossSigning);

        const userSSK = userCrossSigning.keys.self_signing;
        if (!userSSK) {
            // if the user has no self-signing key then we cannot make any
            // trust assertions about this device from cross-signing
            return new DeviceTrustLevel(false, false, localTrust);
        }

        const deviceObj = deviceToObject(device, userCrossSigning.userId);
        try {
            // if we can verify the user's SSK from their master key...
            pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId);
            // ...and this device's key from their SSK...
            pkVerify(
                deviceObj, publicKeyFromKeyInfo(userSSK)[1], userCrossSigning.userId,
            );
            // ...then we trust this device as much as far as we trust the user
            return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust);
        } catch (e) {
            return new DeviceTrustLevel(false, false, localTrust);
        }
    }
}

function deviceToObject(device, userId) {
    return {
        algorithms: device.algorithms,
        keys: device.keys,
        device_id: device.deviceId,
        user_id: userId,
        signatures: device.signatures,
    };
}

export const CrossSigningLevel = {
    MASTER: 4,
    USER_SIGNING: 2,
    SELF_SIGNING: 1,
};

/**
 * Represents the ways in which we trust a user
 */
export class UserTrustLevel {
    constructor(crossSigningVerified, tofu) {
        this._crossSigningVerified = crossSigningVerified;
        this._tofu = tofu;
    }

    /**
     * @returns {bool} true if this user is verified via any means
     */
    isVerified() {
        return this.isCrossSigningVerified();
    }

    /**
     * @returns {bool} true if this user is verified via cross signing
     */
    isCrossSigningVerified() {
        return this._crossSigningVerified;
    }

    /**
     * @returns {bool} true if this user's key is trusted on first use
     */
    isTofu() {
        return this._tofu;
    }
}

/**
 * Represents the ways in which we trust a device
 */
export class DeviceTrustLevel {
    constructor(crossSigningVerified, tofu, localVerified) {
        this._crossSigningVerified = crossSigningVerified;
        this._tofu = tofu;
        this._localVerified = localVerified;
    }

    static fromUserTrustLevel(userTrustLevel, localVerified) {
        return new DeviceTrustLevel(
            userTrustLevel._crossSigningVerified,
            userTrustLevel._tofu,
            localVerified,
        );
    }

    /**
     * @returns {bool} true if this device is verified via any means
     */
    isVerified() {
        return this.isCrossSigningVerified() || this.isLocallyVerified();
    }

    /**
     * @returns {bool} true if this device is verified via cross signing
     */
    isCrossSigningVerified() {
        return this._crossSigningVerified;
    }

    /**
     * @returns {bool} true if this device is verified locally
     */
    isLocallyVerified() {
        return this._localVerified;
    }

    /**
     * @returns {bool} true if this device is trusted from a user's key
     * that is trusted on first use
     */
    isTofu() {
        return this._tofu;
    }
}
<|endoftext|>


<commit_before>const path = require('path');
const request = require('request');
const NodeCache = require('node-cache');
const AbstractDataSource = require(path.resolve(__dirname, 'abstract-data-source.js'));

/**
 * Todoist widget, todoist is a task organization service.
 */
class Todoist extends AbstractDataSource {
  constructor(config) {
    super(config);

    this._cacheKey = 'todoist';
    this._cache = new NodeCache({ stdTTL: this._config.todoist.cache_in_seconds, checkperiod: 120 });

    this._headers = ['Description', 'Labels'];
    this._setData([['Awaiting data...', '']]);
  }

  /**
   * Gets the Todoist data from either cache or the API.
   *
   */
  getData() {
    const todoistData = this._cache.get(this._cacheKey);

    if (todoistData === undefined) {
      const formData = {
        token: this._config.todoist.api_token,
        sync_token: '*',
        resource_types: '["items", "labels"]',
      };

      request
        .post({ url: 'https://todoist.com/API/v7/sync', formData }, (err, httpResponse, body) => {
          if (err) {
            this._setData([['ERROR!', '']]);

            return;
          }

          if (httpResponse.statusCode === 200) {
            this._cache.set(this._cacheKey, JSON.parse(body));
          }
        });
    } else {
      const items = this._parseTodoistData(todoistData);
      this._setData(items);
    }

    return this._data;
  }

  /**
   * Arranges the Todoist data in items to be displayed.
   *
   * @param data
   * @returns {Array|*}
   * @private
   */
  _parseTodoistData(data) {
    const labels = new Map();
    data.labels.forEach(label => labels.set(label.id, label.name));

    let items = data.items
      .filter(item => Todoist._isTodayOrPastdue(item.due_date_utc))
      .map(item =>
        [
          `◘ ${item.content}`,
          item.labels.sort().reduce((allLabels, labelId) => `${allLabels} ${labels.get(labelId)}`, ''),
        ]
      );

    if (items.length === 0) {
      items = [['No more items for today!', '']];
    }

    return items;
  }

  /**
   * Indicates if the due date is today or its past due.
   *
   * @param dueDateUtc
   * @returns {boolean}
   * @private
   */
  static _isTodayOrPastdue(dueDateUtc) {
    return Date.parse(dueDateUtc) <= (new Date()).setHours(23, 59, 59, 999);
  }

  /**
   * Updates the data to be displayed by the widget.
   *
   * @param items
   * @private
   */
  _setData(items) {
    this._data = {
      headers: this._headers,
      data: items,
    };
  }
}

module.exports = Todoist;
<commit_msg>Add project name at the beginning of a task<commit_after>const path = require('path');
const request = require('request');
const NodeCache = require('node-cache');
const AbstractDataSource = require(path.resolve(__dirname, 'abstract-data-source.js'));

/**
 * Todoist widget, todoist is a task organization service.
 */
class Todoist extends AbstractDataSource {
  constructor(config) {
    super(config);

    this._cacheKey = 'todoist';
    this._cache = new NodeCache({ stdTTL: this._config.todoist.cache_in_seconds, checkperiod: 120 });

    this._headers = ['Description', 'Labels'];
    this._setData([['Awaiting data...', '']]);
  }

  /**
   * Gets the Todoist data from either cache or the API.
   *
   */
  getData() {
    const todoistData = this._cache.get(this._cacheKey);

    if (todoistData === undefined) {
      const formData = {
        token: this._config.todoist.api_token,
        sync_token: '*',
        resource_types: '["items", "labels", "projects"]',
      };

      request
        .post({ url: 'https://todoist.com/API/v7/sync', formData }, (err, httpResponse, body) => {
          if (err) {
            this._setData([['ERROR!', '']]);

            return;
          }

          if (httpResponse.statusCode === 200) {
            this._cache.set(this._cacheKey, JSON.parse(body));
          }
        });
    } else {
      const items = this._parseTodoistData(todoistData);
      this._setData(items);
    }

    return this._data;
  }

  /**
   * Arranges the Todoist data in items to be displayed.
   *
   * @param data
   * @returns {Array|*}
   * @private
   */
  _parseTodoistData(data) {
    const labels = new Map();
    data.labels.forEach(label => labels.set(label.id, label.name));

    const projects = new Map();
    data.projects.forEach(project => projects.set(project.id, project.name));

    let items = data.items
      .filter(item => Todoist._isTodayOrPastdue(item.due_date_utc))
      .map(item =>
        [
          `◘ ${projects.get(item.project_id)}: ${item.content}`,
          item.labels.sort().reduce((allLabels, labelId) => `${allLabels} ${labels.get(labelId)}`, ''),
        ]
      );

    if (items.length === 0) {
      items = [['No more items for today!', '']];
    }

    return items;
  }

  /**
   * Indicates if the due date is today or its past due.
   *
   * @param dueDateUtc
   * @returns {boolean}
   * @private
   */
  static _isTodayOrPastdue(dueDateUtc) {
    return Date.parse(dueDateUtc) <= (new Date()).setHours(23, 59, 59, 999);
  }

  /**
   * Updates the data to be displayed by the widget.
   *
   * @param items
   * @private
   */
  _setData(items) {
    this._data = {
      headers: this._headers,
      data: items,
    };
  }
}

module.exports = Todoist;
<|endoftext|>


<commit_before>/*global describe,it*/
'use strict';
var sync = require("../../lib/sync/comment"),
  opsModule = require("../../lib/sync/ops"),
  item = require("../../lib/item"),
  comment = require("../../lib/comment"),
  cache = require("../../lib/cache"),
  should = require("should");

describe("sync/comment", function () {
  describe("diff", function () {
    it("creates comments in github", function () {
      var asanaComments = [
          comment.create({
            body : "create in github",
            fields : {"aa.id" : "1"}
          })
        ],
        gitHubComments = [],
        expectedOperations = opsModule.create({
          github : {
            parent : null,
            create : [asanaComments[0]],
            update : [],
            del : []
          }
        });

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("creates comments in asana", function () {
      var asanaComments = [],
        gitHubComments = [
          comment.create({
            body : "create in asana",
            fields : {"gh.id" : "1"}
          })
        ],
        expectedOperations = opsModule.create({
          asana : {
            parent : null,
            create : [gitHubComments[0]],
            update : [],
            del : []
          }
        });

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("updates nothing if comments are identical", function () {
      var asanaComments = [
          comment.create({
            body : "update in asana - identical",
            lastUpdated : 1,
            fields : {"aa.id" : "1", foo : "bar"}
          })
        ],
        gitHubComments = [
          comment.create({
            body : "update in asana - identical",
            lastUpdated : 3,
            fields : {"aa.id" : "1", baz : "quz"}
          })
        ],
        expectedOperations = opsModule.create();

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("updates comment (originally created in asana) in asana if github is more recent", function () {
      var asanaComments = [
          comment.create({
            body : "update in asana - asana",
            lastUpdated : 1,
            fields : {"aa.id" : "1"}
          })
        ],
        gitHubComments = [
          comment.create({
            body : "update in asana - github",
            lastUpdated : 3,
            fields : {"aa.id" : "1"}
          })
        ],
        expectedOperations = opsModule.create({
          asana : {
            parent : null,
            create : [],
            update : [
              {
                old : asanaComments[0],
                nue : gitHubComments[0]
              }
            ],
            del : []
          }
        });

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("updates comment (originally created in asana) in github if asana is more recent", function () {
      var asanaComments = [
          comment.create({
            body : "update in github - asana",
            lastUpdated : 3,
            fields : {"aa.id" : "1"}
          })
        ],
        gitHubComments = [
          comment.create({
            body : "update in github - github",
            lastUpdated : 1,
            fields : {"aa.id" : "1"}
          })
        ],
        expectedOperations = opsModule.create({
          github : {
            parent : null,
            create : [],
            update : [
              {
                old : gitHubComments[0],
                nue : asanaComments[0]
              }
            ],
            del : []
          }
        });

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("updates comment (originally created in github) in asana if github is more recent", function () {
      var asanaComments = [
          comment.create({
            body : "update in asana - asana",
            lastUpdated : 1,
            fields : {"gh.id" : "1"}
          })
        ],
        gitHubComments = [
          comment.create({
            body : "update in asana - github",
            lastUpdated : 3,
            fields : {"gh.id" : "1"}
          })
        ],
        expectedOperations = opsModule.create({
          asana : {
            parent : null,
            create : [],
            update : [
              {
                old : asanaComments[0],
                nue : gitHubComments[0]
              }
            ],
            del : []
          }
        });

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("updates comment (originally created in github) in github if asana is more recent", function () {
      var asanaComments = [
          comment.create({
            body : "update in github - asana",
            lastUpdated : 3,
            fields : {"gh.id" : "1"}
          })
        ],
        gitHubComments = [
          comment.create({
            body : "update in github - github",
            lastUpdated : 1,
            fields : {"gh.id" : "1"}
          })
        ],
        expectedOperations = opsModule.create({
          github : {
            parent : null,
            create : [],
            update : [
              {
                old : gitHubComments[0],
                nue : asanaComments[0]
              }
            ],
            del : []
          }
        });

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("deletes comments in github if find a github comment with an asana id but no matching asana comment", function () {
      var asanaComments = [],
        gitHubComments = [
          comment.create({
            body : "delete in github because no matching asana comment",
            fields : {"aa.id" : "1"}
          })
        ],
        expectedOperations = opsModule.create({
          github : {
            parent : null,
            create : [],
            update : [],
            del : [gitHubComments[0]]
          }
        });

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("deletes comments in asana if find a asana comment with a github id but no matching github comment", function () {
      var asanaComments = [
          comment.create({
            body : "delete in asana because no matching github comment",
            fields : {"gh.id" : "1"}
          })
        ],
        gitHubComments = [],
        expectedOperations = opsModule.create({
          asana : {
            parent : null,
            create : [],
            update : [],
            del : [asanaComments[0]]
          }
        });

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

  });

  // *******************************************************************
  describe("hasExecutionError", function () {
    it("returns true if comment create error", function () {
      var ops = opsModule.create({
          asana : {
            create : [
              comment.create({error : {text : "error"}}),
              comment.create()
            ],
            update : [],
            del : []
          }
        });

      sync.hasExecutionError(ops).should.eql(true);
    });

    it("returns true if github comment update error", function () {
      var ops = opsModule.create({
          github : {
            create : [comment.create()],
            update : [{nue : comment.create({error : {text : "error"}})}],
            del : [comment.create()]
          }
        });

      sync.hasExecutionError(ops).should.eql(true);
    });

    it("returns false if no comment errors", function () {
      var ops = opsModule.create({
          asana : {
            create : [comment.create(), comment.create()],
            update : [{nue : comment.create()}],
            del : [comment.create()]
          },
          github : {
            create : [comment.create()],
            update : [{nue : comment.create()}, {nue : comment.create()}],
            del : [comment.create()]
          }
        });

      sync.hasExecutionError(ops).should.eql(false);
    });

    it("returns false if errors due to unsupported operations", function () {
      var ops = opsModule.create({
          asana : {
            create : [comment.create({error : {unsupported : true}})],
            update : [{nue : comment.create({error : {unsupported : true}})}],
            del : [comment.create({error : {unsupported : true}})]
          },
          github : {
            create : [comment.create({error : {unsupported : true}})],
            update : [{nue : comment.create({error : {unsupported : true}})}],
            del : [comment.create({error : {unsupported : true}})]
          }
        });

      sync.hasExecutionError(ops).should.eql(false);
    });
  });

  // *******************************************************************
  // describe.skip("sync comments", function () {
  //   it("must sync the comments of the specified items", function () {
  //     var asanaItem = item.create({
  //           title : "#33 hello world",
  //           managerId : "18935613894395"
  //         }),
  //         gitHubItem = item.create({
  //         title : "hello world",
  //         fields : {
  //           "gh.owner" : "marcboscher",
  //           "gh.repo" : "cctest",
  //           "gh.number" : 33
  //         }
  //       });

  //     return sync.getOps(asanaItem, gitHubItem)
  //       .then(function (ops) {
  //         console.log("\n\n####OPS#####\n%s\n\n", JSON.stringify(ops, null, 2));

  //         return sync.execOps(ops, asanaItem, gitHubItem);
  //       });
  //   });
  // });

});
<commit_msg>Update a test (functionality has changed)<commit_after>/*global describe,it*/
'use strict';
var sync = require("../../lib/sync/comment"),
  opsModule = require("../../lib/sync/ops"),
  item = require("../../lib/item"),
  comment = require("../../lib/comment"),
  cache = require("../../lib/cache"),
  should = require("should");

describe("sync/comment", function () {
  describe("diff", function () {
    it("creates comments in github", function () {
      var asanaComments = [
          comment.create({
            body : "create in github",
            fields : {"aa.id" : "1"}
          })
        ],
        gitHubComments = [],
        expectedOperations = opsModule.create({
          github : {
            parent : null,
            create : [asanaComments[0]],
            update : [],
            del : []
          }
        });

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("creates comments in asana", function () {
      var asanaComments = [],
        gitHubComments = [
          comment.create({
            body : "create in asana",
            fields : {"gh.id" : "1"}
          })
        ],
        expectedOperations = opsModule.create({
          asana : {
            parent : null,
            create : [gitHubComments[0]],
            update : [],
            del : []
          }
        });

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("updates nothing if comments are identical", function () {
      var asanaComments = [
          comment.create({
            body : "update in asana - identical",
            lastUpdated : 1,
            fields : {"aa.id" : "1", foo : "bar"}
          })
        ],
        gitHubComments = [
          comment.create({
            body : "update in asana - identical",
            lastUpdated : 3,
            fields : {"aa.id" : "1", baz : "quz"}
          })
        ],
        expectedOperations = opsModule.create();

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("updates comment (originally created in asana) in asana if github is more recent", function () {
      var asanaComments = [
          comment.create({
            body : "update in asana - asana",
            lastUpdated : 1,
            fields : {"aa.id" : "1"}
          })
        ],
        gitHubComments = [
          comment.create({
            body : "update in asana - github",
            lastUpdated : 3,
            fields : {"aa.id" : "1"}
          })
        ],
        expectedOperations = opsModule.create({
          asana : {
            parent : null,
            create : [],
            update : [
              {
                old : asanaComments[0],
                nue : gitHubComments[0]
              }
            ],
            del : []
          }
        });

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("updates comment (originally created in asana) in github if asana is more recent", function () {
      var asanaComments = [
          comment.create({
            body : "update in github - asana",
            lastUpdated : 3,
            fields : {"aa.id" : "1"}
          })
        ],
        gitHubComments = [
          comment.create({
            body : "update in github - github",
            lastUpdated : 1,
            fields : {"aa.id" : "1"}
          })
        ],
        expectedOperations = opsModule.create({
          github : {
            parent : null,
            create : [],
            update : [
              {
                old : gitHubComments[0],
                nue : asanaComments[0]
              }
            ],
            del : []
          }
        });

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("updates comment (originally created in github) in asana if github is more recent", function () {
      var asanaComments = [
          comment.create({
            body : "update in asana - asana",
            lastUpdated : 1,
            fields : {"gh.id" : "1"}
          })
        ],
        gitHubComments = [
          comment.create({
            body : "update in asana - github",
            lastUpdated : 3,
            fields : {"gh.id" : "1"}
          })
        ],
        expectedOperations = opsModule.create({
          asana : {
            parent : null,
            create : [],
            update : [
              {
                old : asanaComments[0],
                nue : gitHubComments[0]
              }
            ],
            del : []
          }
        });

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("updates comment (originally created in github) in github if asana is more recent", function () {
      var asanaComments = [
          comment.create({
            body : "update in github - asana",
            lastUpdated : 3,
            fields : {"gh.id" : "1"}
          })
        ],
        gitHubComments = [
          comment.create({
            body : "update in github - github",
            lastUpdated : 1,
            fields : {"gh.id" : "1"}
          })
        ],
        expectedOperations = opsModule.create({
          github : {
            parent : null,
            create : [],
            update : [
              {
                old : gitHubComments[0],
                nue : asanaComments[0]
              }
            ],
            del : []
          }
        });

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("adds comments in asana if find a github comment with an asana id but no matching asana comment", function () {
      var asanaComments = [],
        gitHubComments = [
          comment.create({
            body : "created in asana because there is no matching asana comment",
            fields : {"aa.id" : "1"}
          })
        ],
        expectedOperations = opsModule.create({
          asana : {
            parent : null,
            create : [gitHubComments[0]],
            update : [],
            del : []
          }
        });
      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

    it("deletes comments in asana if find a asana comment with a github id but no matching github comment", function () {
      var asanaComments = [
          comment.create({
            body : "delete in asana because no matching github comment",
            fields : {"gh.id" : "1"}
          })
        ],
        gitHubComments = [],
        expectedOperations = opsModule.create({
          asana : {
            parent : null,
            create : [],
            update : [],
            del : [asanaComments[0]]
          }
        });

      sync.diff(asanaComments, gitHubComments).should.eql(expectedOperations);
    });

  });

  // *******************************************************************
  describe("hasExecutionError", function () {
    it("returns true if comment create error", function () {
      var ops = opsModule.create({
          asana : {
            create : [
              comment.create({error : {text : "error"}}),
              comment.create()
            ],
            update : [],
            del : []
          }
        });

      sync.hasExecutionError(ops).should.eql(true);
    });

    it("returns true if github comment update error", function () {
      var ops = opsModule.create({
          github : {
            create : [comment.create()],
            update : [{nue : comment.create({error : {text : "error"}})}],
            del : [comment.create()]
          }
        });

      sync.hasExecutionError(ops).should.eql(true);
    });

    it("returns false if no comment errors", function () {
      var ops = opsModule.create({
          asana : {
            create : [comment.create(), comment.create()],
            update : [{nue : comment.create()}],
            del : [comment.create()]
          },
          github : {
            create : [comment.create()],
            update : [{nue : comment.create()}, {nue : comment.create()}],
            del : [comment.create()]
          }
        });

      sync.hasExecutionError(ops).should.eql(false);
    });

    it("returns false if errors due to unsupported operations", function () {
      var ops = opsModule.create({
          asana : {
            create : [comment.create({error : {unsupported : true}})],
            update : [{nue : comment.create({error : {unsupported : true}})}],
            del : [comment.create({error : {unsupported : true}})]
          },
          github : {
            create : [comment.create({error : {unsupported : true}})],
            update : [{nue : comment.create({error : {unsupported : true}})}],
            del : [comment.create({error : {unsupported : true}})]
          }
        });

      sync.hasExecutionError(ops).should.eql(false);
    });
  });

  // *******************************************************************
  // describe.skip("sync comments", function () {
  //   it("must sync the comments of the specified items", function () {
  //     var asanaItem = item.create({
  //           title : "#33 hello world",
  //           managerId : "18935613894395"
  //         }),
  //         gitHubItem = item.create({
  //         title : "hello world",
  //         fields : {
  //           "gh.owner" : "marcboscher",
  //           "gh.repo" : "cctest",
  //           "gh.number" : 33
  //         }
  //       });

  //     return sync.getOps(asanaItem, gitHubItem)
  //       .then(function (ops) {
  //         console.log("\n\n####OPS#####\n%s\n\n", JSON.stringify(ops, null, 2));

  //         return sync.execOps(ops, asanaItem, gitHubItem);
  //       });
  //   });
  // });

});
<|endoftext|>


<commit_before>// Project model
const Project = require('../models/project');

// Portfolio model
const Portfolio = require('../models/portfolio');

module.exports = {
    createDummy: createDummy
}

/**
 * Create a dummy portfolio for logged user
 * @param {request} req
 * @param {response} res
 */
function createDummy(req, res) {
    // create dummy portfolio
    const newUserPortfolio = new Portfolio();
    newUserPortfolio.createdBy = req.user.id;
    newUserPortfolio.projectList = [];

    const project1 = new Project();
    project1.createdBy = req.user.id;
    project1.name = 'reacttocurrex';
    project1.title = 'React to Currency Exchange';
    project1.briefDescription = '"React to Currency Exchange" is a one-page application built using ReactJS';
    project1.detailedDescription = '"React to Currency Exchange" is a one-page application built using ReactJS. It computes currency exchange by using the latest exchange rates provided by the fixer.io API
https://evildead.github.io/reacttocurrex';
    project1.mediaList = [{
            mediaUrl: 'https://www.broadlandshoa.org/wp-content/uploads/2016/02/basketball_hoop.png',
            description: 'Throwing into a basket.'
        },
        {
            mediaUrl: 'http://www.nhs.uk/Livewell/fitness/PublishingImages/getting%20started%20guides/T_1216_swimming_612848960_A.jpg',
            description: 'Michael Phelps is the fast fish.'
        },
        {
            mediaUrl: 'http://tamildiplomat.com/wp-content/uploads/2016/09/Weight-Lifting.gif',
            description: 'Lifting heavy things up'
        }
    ];
    // save the project1
    project1.save((err) => {
        if(err) {
            throw err;
        }
        newUserPortfolio.projectList.push(project1.id);
    });

    const project2 = new Project();
    project2.createdBy = req.user.id;
    project2.name = 'create-a-crud-app-with-node-and-mongodb';
    project2.title = 'Create a CRUD App with NodeJS and Mongodb';
    project2.briefDescription = 'This is a CRUD application built with NodeJS in a Scotch.io course';
    project2.detailedDescription = 'This is a CRUD application built with NodeJS in a Scotch.io course
It features: Create, Read, Update, Delete of Olympics events';
    project2.mediaList = [{
            mediaUrl: 'http://www.bloglet.com/gallery/country-artists/country-artists.png',
            description: 'Country is beautiful.'
        },
        {
            mediaUrl: 'https://pbs.twimg.com/profile_images/755476864563613697/z1GgG2Ht.jpg',
            description: 'Hard Rock kicks asses!'
        },
        {
            mediaUrl: 'https://i.pinimg.com/736x/42/ca/bd/42cabd4006153a3856f7a5863dff8549--heavy-metal-music-heavy-metal-bands.jpg',
            description: 'Heavy Metal is the law \mm/'
        }
    ];
    // save the project2
    project2.save((err) => {
        if(err) {
            throw err;
        }
        newUserPortfolio.projectList.push(project2.id);
    });

    // save the portfolio
    newUserPortfolio.save((err) => {
        if(err) {
            throw err;
        }

        // set a successful flash message
        req.flash('success', 'Successfully created dummy portfolio!');

        // redirect to the newly created portfolio
        res.redirect(`/portfolios/${req.user.google.id}`);
    });

    // seeded!
    res.send('Portfolio created!');
}
<commit_msg>Created dummy portfolio<commit_after>// Project model
const Project = require('../models/project');

// Portfolio model
const Portfolio = require('../models/portfolio');

module.exports = {
    createDummy: createDummy
}

/**
 * Create a dummy portfolio for logged user
 * @param {request} req
 * @param {response} res
 */
function createDummy(req, res) {
    // create dummy portfolio
    const newUserPortfolio = new Portfolio();
    newUserPortfolio.createdBy = req.user.id;
    newUserPortfolio.projectList = [];

    const project1 = new Project();
    project1.createdBy = req.user.id;
    project1.name = 'reacttocurrex';
    project1.title = 'React to Currency Exchange';
    project1.briefDescription = '"React to Currency Exchange" is a one-page application built using ReactJS';
    project1.detailedDescription = '"React to Currency Exchange" is a one-page application built using ReactJS. It computes currency exchange by using the latest exchange rates provided by the fixer.io API
https://evildead.github.io/reacttocurrex';
    project1.mediaList = [{
            mediaUrl: 'https://www.broadlandshoa.org/wp-content/uploads/2016/02/basketball_hoop.png',
            description: 'Throwing into a basket.'
        },
        {
            mediaUrl: 'http://www.nhs.uk/Livewell/fitness/PublishingImages/getting%20started%20guides/T_1216_swimming_612848960_A.jpg',
            description: 'Michael Phelps is the fast fish.'
        },
        {
            mediaUrl: 'http://tamildiplomat.com/wp-content/uploads/2016/09/Weight-Lifting.gif',
            description: 'Lifting heavy things up'
        }
    ];
    // save the project1
    var promise1 = project1.save((err) => {
        if(err) {
            throw err;
        }
        newUserPortfolio.projectList.push(project1.id);
    });

    const project2 = new Project();
    project2.createdBy = req.user.id;
    project2.name = 'create-a-crud-app-with-node-and-mongodb';
    project2.title = 'Create a CRUD App with NodeJS and Mongodb';
    project2.briefDescription = 'This is a CRUD application built with NodeJS in a Scotch.io course';
    project2.detailedDescription = 'This is a CRUD application built with NodeJS in a Scotch.io course
It features: Create, Read, Update, Delete of Olympics events';
    project2.mediaList = [{
            mediaUrl: 'http://www.bloglet.com/gallery/country-artists/country-artists.png',
            description: 'Country is beautiful.'
        },
        {
            mediaUrl: 'https://pbs.twimg.com/profile_images/755476864563613697/z1GgG2Ht.jpg',
            description: 'Hard Rock kicks asses!'
        },
        {
            mediaUrl: 'https://i.pinimg.com/736x/42/ca/bd/42cabd4006153a3856f7a5863dff8549--heavy-metal-music-heavy-metal-bands.jpg',
            description: 'Heavy Metal is the law \mm/'
        }
    ];
    // save the project2
    var promise2 = project2.save((err) => {
        if(err) {
            throw err;
        }
        newUserPortfolio.projectList.push(project2.id);
    });

    Promise.all([promise1, promise2]).then(values => {
        console.log(values);

        // save the portfolio
        newUserPortfolio.save((err) => {
            if(err) {
                throw err;
            }

            // set a successful flash message
            req.flash('success', 'Successfully created dummy portfolio!');

            // redirect to the newly created portfolio
            res.redirect(`/portfolios/${req.user.google.id}`);
        });
    });
}
<|endoftext|>


<commit_before>/*! Simple CSS loader asynchronously for all browsers by cara-tm.com, MIT Licence */
function LoadCSS(e,r){"use strict";var b="";-1==b.indexOf("["+e+"]")?(Loader(e,r),b+="["+e+"]"):alert("File "+e+" already added!")}function Loader(e,r){var s=document.createElement("link");s.setAttribute("rel","stylesheet"),s.setAttribute("href",e),r?"":r="all",s.setAttribute("media",r),s.setAttribute("type","text/css")if("undefined"!=typeof s){var a=document.getElementsByTagName("head");a=a[a.length-1],a.appendChild(s)}
<commit_msg>Update LoadCSS.min.js<commit_after>/*! Simple CSS loader asynchronously for all browsers by cara-tm.com, MIT Licence */
function LoadCSS(e,m){"use strict";var b="";-1==b.indexOf("["+e+"]")?(Loader(e,m),b+="["+e+"]"):alert("File "+e+" already added!")}function Loader(e,m){var s=document.createElement("link");s.setAttribute("rel","stylesheet"),s.setAttribute("href",e),m?"":m="all",s.setAttribute("media",m),s.setAttribute("type","text/css");if("undefined"!=typeof s){var a=document.getElementsByTagName("head");a=a[a.length-1],a.appendChild(s)}}
<|endoftext|>


<commit_before>
var id = function (id){ return document.getElementById(id); };

var tf = new TableFilter('demo', {
    base_path: '../dist/tablefilter/',
    col_2: 'multiple',
    col_3: 'select',
    col_4: 'none',
    popup_filters: true
});
tf.init();

var popupFilter = tf.feature('popupFilter');
module('Sanity checks');
test('Pop-up filter component', function() {
    notEqual(popupFilter, null, 'PopupFilter instanciated');
    deepEqual(popupFilter.fltElms instanceof Array,
        true, 'Type of fltElms property');
    deepEqual(
        popupFilter.adjustToContainer,
        true,
        'Popup filter width adjusts to container'
    );
    deepEqual(tf.headersRow, 1, 'Headers row index');
    // issue 99: getHeadersText for pick-list filter types
    deepEqual(
        tf.getHeadersText()[2],
        'Road Distance (km)',
        'Expected header text for multiple filter type'
    );
    deepEqual(
        tf.getHeadersText()[3],
        'By Air (hrs)',
        'Expected header text for multiple filter type'
    );
});

module('UI elements');
test('Pop-up filter UI elements', function() {
    var flt1 = id(tf.fltIds[3]);
    var flt2 = id(tf.fltIds[2]);
    var fltIcn1 = popupFilter.fltIcons[3];
    var fltIcn2 = popupFilter.fltIcons[2];
    var fltIcn3 = popupFilter.fltIcons[4];

    notEqual(flt1, null, 'Filter element exists');
    notEqual(flt2, null, 'Filter element exists');
    deepEqual(flt2.hasAttribute('multiple'), true, 'Multiple select exists');
    deepEqual(fltIcn1.nodeName, 'IMG', 'Filter icon exists');
    deepEqual(fltIcn2.nodeName, 'IMG', 'Filter icon exists');
    deepEqual(fltIcn3, undefined, 'Filter icon does not exist for column 4');
});

test('Pop-up filter state after filtering', function(){
    var fltIcn0 = popupFilter.fltIcons[0];
    tf.setFilterValue(0, 'syd');
    tf.filter();

    deepEqual(fltIcn0.src.indexOf('icn_filterActive') !== -1,
        true, 'Icon state');
});

test('Pop-up filter state after clearing', function(){
    var fltIcn0 = popupFilter.fltIcons[0];
    var fltIcn1 = popupFilter.fltIcons[1];
    var fltIcn2 = popupFilter.fltIcons[2];
    var fltIcn3 = popupFilter.fltIcons[3];
    tf.clearFilters();

    deepEqual(fltIcn0.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
    deepEqual(fltIcn1.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
    deepEqual(fltIcn2.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
    deepEqual(fltIcn3.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
});

test('Can open pop-up filter', function(){
    // act
    popupFilter.open(1);

    // assert
    deepEqual(popupFilter.fltElms[1].style.display, 'block',
        'Popup filter is open');
});

test('Can close pop-up filter', function(){
    // act
    popupFilter.close(1);

    // assert
    deepEqual(popupFilter.fltElms[1].style.display, 'none',
        'Popup filter is open');
});

test('Can toggle pop-up filter (initially closed)', function(){
    // setup
    popupFilter.close(2);

    // act
    popupFilter.toggle(2);

    // assert
    deepEqual(popupFilter.fltElms[2].style.display, 'block',
        'Popup filter is toggled');
});

test('Can toggle pop-up filter (initially opened)', function(){
    // setup
    popupFilter.open(2);

    // act
    popupFilter.toggle(2);

    // assert
    deepEqual(popupFilter.fltElms[2].style.display, 'none',
        'Popup filter is toggled');
});

test('Multiple selection pop-up filter remains open upon filtering', function(){
    // setup
    popupFilter.open(2);

    // act
    tf.setFilterValue(2, ['1412', '982']);
    tf.filter();

    // assert
    deepEqual(
        popupFilter.isOpen(2),
        true,
        'Multiple selection pop-up filter still open after filtering'
    );
});

test('Pop-up filter closes upon filtering', function(){
    // setup
    popupFilter.open(1);

    // act
    tf.setFilterValue(1, 'Adelaide');
    tf.filter();

    // assert
    deepEqual(popupFilter.isOpen(1), false,
        'Pop-up filter closed after filtering'
    );
});

test('Pop-up filter auto-closes when user clicks away', function() {
    // setup
    popupFilter.open(0);

    // act
    var evObj = document.createEvent('HTMLEvents');
    evObj.initEvent('mouseup', true, true);
    tf.dom().rows[4].cells[2].dispatchEvent(evObj);

    // assert
    deepEqual(popupFilter.isOpen(0), false,
        'Pop-up filter closed after user clicks away'
    );
});

test('Can close all popup filters', function() {
    // setup
    popupFilter.open(0);

    // act
    popupFilter.closeAll();

    // assert
    deepEqual(popupFilter.isOpen(0), false,
        'Pop-up filter closed after closeAll'
    );
});

test('Can destroy and reset', function(){
    // setup
    popupFilter.destroy();

    // act
    popupFilter.reset();

    // assert
    deepEqual(popupFilter.fltElms.length, 5, 'Filters are generated');
    deepEqual(popupFilter.fltIcons.length, 4, 'Icons are generated');
    deepEqual(popupFilter.fltSpans.length, 4, 'Icon containers are generated');
});

test('TableFilter removed', function() {
    tf.destroy();
    var fltIcn1 = popupFilter.fltIcons[3];
    deepEqual(fltIcn1, undefined, 'Filter icon is removed');
    deepEqual(id(tf.fltIds[3]), null, 'Filter is removed');
});

test('TableFilter re-initialised', function() {
    tf.init();
    var fltIcn1 = popupFilter.fltIcons[3];
    deepEqual(fltIcn1.nodeName, 'IMG', 'Filter icon exists');
    deepEqual(id(tf.fltIds[3]).nodeName, 'SELECT', 'Filter exists');
});

module('Grid-layout');
test('Re-instantiated with grid-layout', function() {
    tf.destroy();
    tf = null;
    tf = new TableFilter('demo', {
        base_path: '../dist/tablefilter/',
        col_2: 'multiple',
        col_3: 'select',
        col_4: 'none',
        popup_filters: true,
        grid_layout: true
    });
    tf.init();

    notEqual(popupFilter, null, 'PopupFilter instanciated');
    deepEqual(popupFilter.fltElms instanceof Array,
        true, 'Type of fltElms property');
    deepEqual(tf.headersRow, 0, 'Headers row index');
    // issue 99: getHeadersText for pick-list filter types
    deepEqual(
        tf.getHeadersText()[2],
        'Road Distance (km)',
        'Expected header text for multiple filter type'
    );
    deepEqual(
        tf.getHeadersText()[3],
        'By Air (hrs)',
        'Expected header text for multiple filter type'
    );
});

test('Pop-up filter UI elements with grid-layout', function() {
    var popupFilter = tf.feature('popupFilter');
    var flt1 = id(tf.fltIds[3]);
    var flt2 = id(tf.fltIds[2]);
    var fltIcn1 = popupFilter.fltIcons[3];
    var fltIcn2 = popupFilter.fltIcons[2];
    var fltIcn3 = popupFilter.fltIcons[4];

    notEqual(flt1, null, 'Filter element exists');
    notEqual(flt2, null, 'Filter element exists');
    deepEqual(flt2.hasAttribute('multiple'), true, 'Multiple select exists');
    deepEqual(fltIcn1.nodeName, 'IMG', 'Filter icon exists');
    deepEqual(fltIcn2.nodeName, 'IMG', 'Filter icon exists');
    deepEqual(fltIcn3, undefined, 'Filter icon does not exist for column 4');
});

test('Pop-up filter state after filtering', function(){
    var popupFilter = tf.feature('popupFilter');
    var fltIcn0 = popupFilter.fltIcons[0];
    tf.setFilterValue(0, 'syd');
    tf.filter();

    deepEqual(fltIcn0.src.indexOf('icn_filterActive') !== -1,
        true, 'Icon state');
});

test('Pop-up filter state after clearing', function(){
    var popupFilter = tf.feature('popupFilter');
    var fltIcn0 = popupFilter.fltIcons[0];
    var fltIcn1 = popupFilter.fltIcons[1];
    var fltIcn2 = popupFilter.fltIcons[2];
    var fltIcn3 = popupFilter.fltIcons[3];
    tf.clearFilters();

    deepEqual(fltIcn0.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
    deepEqual(fltIcn1.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
    deepEqual(fltIcn2.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
    deepEqual(fltIcn3.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
});

module('Feature interface');
test('Properties', function() {
    var popupFilter = tf.feature('popupFilter');

    deepEqual(popupFilter.tf instanceof TableFilter,
        true, 'TableFilter instance');
    deepEqual(popupFilter.feature, 'popupFilters', 'Feature name');
    deepEqual(popupFilter.enabled, true, 'Feature enabled');
    deepEqual(popupFilter.initialized, true, 'Feature enabled');
    deepEqual(typeof popupFilter.emitter, 'object',
        'Feature has emitter instance');
    deepEqual(typeof popupFilter.config, 'object', 'TF configuration object');
    deepEqual(typeof popupFilter.init, 'function', 'Feature init method');
    deepEqual(typeof popupFilter.destroy, 'function', 'Feature destroy method');
    deepEqual(typeof popupFilter.reset, 'function', 'Feature reset method');
    deepEqual(typeof popupFilter.enable, 'function', 'Feature enable method');
    deepEqual(typeof popupFilter.disable, 'function', 'Feature enable method');
    deepEqual(typeof popupFilter.isEnabled,
        'function', 'Feature enable method');
});

module('Tear-down');
test('TableFilter removed', function() {
    tf.destroy();
    var fltIcn1 = popupFilter.fltIcons[3];
    deepEqual(fltIcn1, undefined, 'Filter icon is removed');
    deepEqual(id(tf.fltIds[3]), null, 'Filter is removed');
    deepEqual(tf.isInitialized(), false, 'Filters removed');
});
<commit_msg>Expanded unit tests<commit_after>
var id = function (id){ return document.getElementById(id); };

var tf = new TableFilter('demo', {
    base_path: '../dist/tablefilter/',
    col_2: 'multiple',
    col_3: 'select',
    col_4: 'none',
    popup_filters: true
});
tf.init();

var popupFilter = tf.feature('popupFilter');
module('Sanity checks');
test('Pop-up filter component', function() {
    notEqual(popupFilter, null, 'PopupFilter instanciated');
    deepEqual(popupFilter.fltElms instanceof Array,
        true, 'Type of fltElms property');
    deepEqual(
        popupFilter.adjustToContainer,
        true,
        'Popup filter width adjusts to container'
    );
    deepEqual(tf.headersRow, 1, 'Headers row index');
    // issue 99: getHeadersText for pick-list filter types
    deepEqual(
        tf.getHeadersText()[2],
        'Road Distance (km)',
        'Expected header text for multiple filter type'
    );
    deepEqual(
        tf.getHeadersText()[3],
        'By Air (hrs)',
        'Expected header text for multiple filter type'
    );
});

module('UI elements');
test('Pop-up filter UI elements', function() {
    var flt1 = id(tf.fltIds[3]);
    var flt2 = id(tf.fltIds[2]);
    var fltIcn1 = popupFilter.fltIcons[3];
    var fltIcn2 = popupFilter.fltIcons[2];
    var fltIcn3 = popupFilter.fltIcons[4];

    notEqual(flt1, null, 'Filter element exists');
    notEqual(flt2, null, 'Filter element exists');
    deepEqual(flt2.hasAttribute('multiple'), true, 'Multiple select exists');
    deepEqual(fltIcn1.nodeName, 'IMG', 'Filter icon exists');
    deepEqual(fltIcn2.nodeName, 'IMG', 'Filter icon exists');
    deepEqual(fltIcn3, undefined, 'Filter icon does not exist for column 4');
});

test('Pop-up filter state after filtering', function(){
    var fltIcn0 = popupFilter.fltIcons[0];
    tf.setFilterValue(0, 'syd');
    tf.filter();

    deepEqual(fltIcn0.src.indexOf('icn_filterActive') !== -1,
        true, 'Icon state');
});

test('Pop-up filter state after clearing', function(){
    var fltIcn0 = popupFilter.fltIcons[0];
    var fltIcn1 = popupFilter.fltIcons[1];
    var fltIcn2 = popupFilter.fltIcons[2];
    var fltIcn3 = popupFilter.fltIcons[3];
    tf.clearFilters();

    deepEqual(fltIcn0.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
    deepEqual(fltIcn1.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
    deepEqual(fltIcn2.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
    deepEqual(fltIcn3.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
});

test('Can open pop-up filter', function(){
    // act
    popupFilter.open(1);

    // assert
    deepEqual(popupFilter.fltElms[1].style.display, 'block',
        'Popup filter is open');
});

test('Can close pop-up filter', function(){
    // act
    popupFilter.close(1);

    // assert
    deepEqual(popupFilter.fltElms[1].style.display, 'none',
        'Popup filter is open');
});

test('Can toggle pop-up filter (initially closed)', function(){
    // setup
    popupFilter.close(2);

    // act
    popupFilter.toggle(2);

    // assert
    deepEqual(popupFilter.fltElms[2].style.display, 'block',
        'Popup filter is toggled');
});

test('Can toggle pop-up filter (initially opened)', function(){
    // setup
    popupFilter.open(2);

    // act
    popupFilter.toggle(2);

    // assert
    deepEqual(popupFilter.fltElms[2].style.display, 'none',
        'Popup filter is toggled');
});

test('Multiple selection pop-up filter remains open upon filtering', function(){
    // setup
    popupFilter.open(2);

    // act
    tf.setFilterValue(2, ['1412', '982']);
    tf.filter();

    // assert
    deepEqual(
        popupFilter.isOpen(2),
        true,
        'Multiple selection pop-up filter still open after filtering'
    );
});

test('Pop-up filter closes upon filtering', function(){
    // setup
    popupFilter.open(1);

    // act
    tf.setFilterValue(1, 'Adelaide');
    tf.filter();

    // assert
    deepEqual(popupFilter.isOpen(1), false,
        'Pop-up filter closed after filtering'
    );
});

test('Pop-up filter auto-closes when user clicks away', function() {
    // setup
    popupFilter.open(0);

    // act
    var evObj = document.createEvent('HTMLEvents');
    evObj.initEvent('mouseup', true, true);
    tf.dom().rows[4].cells[2].dispatchEvent(evObj);

    // assert
    deepEqual(popupFilter.isOpen(0), false,
        'Pop-up filter closed after user clicks away'
    );
});

test('Can close all popup filters', function() {
    // setup
    popupFilter.open(0);

    // act
    popupFilter.closeAll();

    // assert
    deepEqual(popupFilter.isOpen(0), false,
        'Pop-up filter closed after closeAll'
    );
});

test('Can destroy and reset', function(){
    // setup
    popupFilter.destroy();

    // act
    popupFilter.reset();

    // assert
    deepEqual(popupFilter.fltElms.length, 5, 'Filters are generated');
    deepEqual(popupFilter.fltIcons.length, 4, 'Icons are generated');
    deepEqual(popupFilter.fltSpans.length, 4, 'Icon containers are generated');
});

test('TableFilter removed', function() {
    tf.destroy();
    var fltIcn1 = popupFilter.fltIcons[3];
    deepEqual(fltIcn1, undefined, 'Filter icon is removed');
    deepEqual(id(tf.fltIds[3]), null, 'Filter is removed');
});

test('TableFilter re-initialised', function() {
    tf.init();
    var fltIcn1 = popupFilter.fltIcons[3];
    deepEqual(fltIcn1.nodeName, 'IMG', 'Filter icon exists');
    deepEqual(id(tf.fltIds[3]).nodeName, 'SELECT', 'Filter exists');
});

module('Properties');
test('Can set icon HTML', function() {
    // setup
    tf.destroy();
    tf = new TableFilter('demo', {
        base_path: '../dist/tablefilter/',
        col_2: 'multiple',
        col_3: 'select',
        col_4: 'none',
        popup_filters: {
            image_html: 'hello world'
        }
    });
    var feature = tf.feature('popupFilter');
    feature.filtersCache = [];
    feature.fltElms = [];

    // act
    tf.init();
    var headersRow = tf.dom().rows[tf.getHeadersRowIndex()];

    // assert
    deepEqual(
        headersRow.cells[1].innerHTML
            .indexOf('hello world') !== -1,
        true,
        'Custom HTML element present'
    );
});

module('Grid-layout');
test('Re-instantiated with grid-layout', function() {
    tf.destroy();
    tf = null;
    tf = new TableFilter('demo', {
        base_path: '../dist/tablefilter/',
        col_2: 'multiple',
        col_3: 'select',
        col_4: 'none',
        popup_filters: true,
        grid_layout: true
    });
    tf.init();

    notEqual(popupFilter, null, 'PopupFilter instanciated');
    deepEqual(popupFilter.fltElms instanceof Array,
        true, 'Type of fltElms property');
    deepEqual(tf.headersRow, 0, 'Headers row index');
    // issue 99: getHeadersText for pick-list filter types
    deepEqual(
        tf.getHeadersText()[2],
        'Road Distance (km)',
        'Expected header text for multiple filter type'
    );
    deepEqual(
        tf.getHeadersText()[3],
        'By Air (hrs)',
        'Expected header text for multiple filter type'
    );
});

test('Pop-up filter UI elements with grid-layout', function() {
    var popupFilter = tf.feature('popupFilter');
    var flt1 = id(tf.fltIds[3]);
    var flt2 = id(tf.fltIds[2]);
    var fltIcn1 = popupFilter.fltIcons[3];
    var fltIcn2 = popupFilter.fltIcons[2];
    var fltIcn3 = popupFilter.fltIcons[4];

    notEqual(flt1, null, 'Filter element exists');
    notEqual(flt2, null, 'Filter element exists');
    deepEqual(flt2.hasAttribute('multiple'), true, 'Multiple select exists');
    deepEqual(fltIcn1.nodeName, 'IMG', 'Filter icon exists');
    deepEqual(fltIcn2.nodeName, 'IMG', 'Filter icon exists');
    deepEqual(fltIcn3, undefined, 'Filter icon does not exist for column 4');
});

test('Pop-up filter state after filtering', function(){
    var popupFilter = tf.feature('popupFilter');
    var fltIcn0 = popupFilter.fltIcons[0];
    tf.setFilterValue(0, 'syd');
    tf.filter();

    deepEqual(fltIcn0.src.indexOf('icn_filterActive') !== -1,
        true, 'Icon state');
});

test('Pop-up filter state after clearing', function(){
    var popupFilter = tf.feature('popupFilter');
    var fltIcn0 = popupFilter.fltIcons[0];
    var fltIcn1 = popupFilter.fltIcons[1];
    var fltIcn2 = popupFilter.fltIcons[2];
    var fltIcn3 = popupFilter.fltIcons[3];
    tf.clearFilters();

    deepEqual(fltIcn0.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
    deepEqual(fltIcn1.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
    deepEqual(fltIcn2.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
    deepEqual(fltIcn3.src.indexOf('icn_filterActive') === -1,
        true, 'Icon state');
});

module('Feature interface');
test('Properties', function() {
    var popupFilter = tf.feature('popupFilter');

    deepEqual(popupFilter.tf instanceof TableFilter,
        true, 'TableFilter instance');
    deepEqual(popupFilter.feature, 'popupFilters', 'Feature name');
    deepEqual(popupFilter.enabled, true, 'Feature enabled');
    deepEqual(popupFilter.initialized, true, 'Feature enabled');
    deepEqual(typeof popupFilter.emitter, 'object',
        'Feature has emitter instance');
    deepEqual(typeof popupFilter.config, 'object', 'TF configuration object');
    deepEqual(typeof popupFilter.init, 'function', 'Feature init method');
    deepEqual(typeof popupFilter.destroy, 'function', 'Feature destroy method');
    deepEqual(typeof popupFilter.reset, 'function', 'Feature reset method');
    deepEqual(typeof popupFilter.enable, 'function', 'Feature enable method');
    deepEqual(typeof popupFilter.disable, 'function', 'Feature enable method');
    deepEqual(typeof popupFilter.isEnabled,
        'function', 'Feature enable method');
});

module('Tear-down');
test('TableFilter removed', function() {
    tf.destroy();
    var fltIcn1 = popupFilter.fltIcons[3];
    deepEqual(fltIcn1, undefined, 'Filter icon is removed');
    deepEqual(id(tf.fltIds[3]), null, 'Filter is removed');
    deepEqual(tf.isInitialized(), false, 'Filters removed');
});
<|endoftext|>


<commit_before>export default Ember.ArrayController.extend({
  sortProperties : ['data.publish_date'],
  sortAscending  : false,
  sortedByPublish: true,

  contentType: null,
  lockedItems: Ember.A([]),

  filterQuery: '',

  columnChoices: function () {
    return this.get('contentType.controls').rejectBy('name', 'name').rejectBy('name', 'preview_url');
  }.property('contentType.controls.@each'),

  cmsControls: function () {
    return this.get('contentType.controls').filterBy('showInCms');
  }.property('contentType.controls.@each.showInCms'),

  _updateItemControls: function (item) {
    var cmsControls = Ember.A([]);
    this.get('cmsControls').forEach(function (control) {
      cmsControls.pushObject({
        value: item.get('data')[control.get('name')],
        controlType: control.get('controlType')
      });
    });
    item.set('cmsControls', cmsControls);
    return item;
  },

  sortedByChanged: function () {
    this.setProperties({
      sortedByPublish: false,
      sortedByCreated: false,
      sortedByAlpha: false
    });

    switch (this.get('sortProperties.firstObject')) {
    case 'data.publish_date':
      this.set('sortedByPublish', true);
      break;
    case 'data.create_date':
      this.set('sortedByCreated', true);
      break;
    case 'data.name':
      this.set('sortedByAlpha', true);
      break;
    }

  }.observes('sortProperties'),

  cmsItems: Ember.arrayComputed('model.@each.data', 'cmsControls.@each.showInCms', {
    addedItem: function (array, item, changeMeta) {

      if (item.constructor.typeKey === 'control') {
        array.forEach(this._updateItemControls.bind(this));
      } else {
        array.pushObject(this._updateItemControls(item));
      }

      return array;
    },
    removedItem: function (array, item) {
      if (item.constructor.typeKey !== 'control') {
        array.removeObject(item);
      }
      return array;
    }
  }),

  sortedCmsItems: function () {

    var filterQuery = this.get('filterQuery');

    var sortedCmsItems = this.get('cmsItems').sortBy.apply(this, this.get('sortProperties'));

    if (!this.get('sortAscending')) {
      sortedCmsItems.reverse();
    }

    sortedCmsItems = sortedCmsItems.filter(function (item) {
      if (!filterQuery) {
        return true;
      } else {
        return (new RegExp(filterQuery, 'ig')).test(item.get('data.name'));
      }
    });

    return sortedCmsItems;
  }.property('cmsItems.@each', 'sortProperties', 'sortAscending', 'filterQuery'),

  locksChanged: function () {
    this.get('cmsItems').setEach('lockedBy', null);
    this.get('lockedItems').forEach(function (lock) {
      this.get('cmsItems').findBy('id', lock.get('id')).set('lockedBy', lock.get('email'));
    }, this);
  }.observes('lockedItems.@each'),

  actions: {
    deleteItem: function (item) {
      var id = item.get('id');
      item.destroyRecord().then(function () {
        window.ENV.sendBuildSignal();
        window.ENV.deleteIndex(id, this.get('contentType.id'));
        this.send('notify', 'success', 'Item removed!');
      }.bind(this));
    },

    toggleShowInCms: function (control) {
      control.toggleProperty('showInCms');
      this.get('contentType').save();
    },

    sortToggle: function (field) {

      field = 'data.' + field;

      var sortProperties = this.get('sortProperties');

      if (sortProperties.get('firstObject') === field) {
        this.toggleProperty('sortAscending');
      } else {
        this.set('sortAscending', true);
      }

      sortProperties.insertAt(0, field);
      sortProperties = sortProperties.uniq();

      this.set('sortProperties', sortProperties);

    },

    gotoEdit: function (contentTypeId, itemId) {
      this.transitionToRoute('wh.content.type.edit', contentTypeId, itemId);
    }
  }

});
<commit_msg>probably a good time to add a confirmation on delete<commit_after>export default Ember.ArrayController.extend({
  sortProperties : ['data.publish_date'],
  sortAscending  : false,
  sortedByPublish: true,

  contentType: null,
  lockedItems: Ember.A([]),

  filterQuery: '',

  columnChoices: function () {
    return this.get('contentType.controls').rejectBy('name', 'name').rejectBy('name', 'preview_url');
  }.property('contentType.controls.@each'),

  cmsControls: function () {
    return this.get('contentType.controls').filterBy('showInCms');
  }.property('contentType.controls.@each.showInCms'),

  _updateItemControls: function (item) {
    var cmsControls = Ember.A([]);
    this.get('cmsControls').forEach(function (control) {
      cmsControls.pushObject({
        value: item.get('data')[control.get('name')],
        controlType: control.get('controlType')
      });
    });
    item.set('cmsControls', cmsControls);
    return item;
  },

  sortedByChanged: function () {
    this.setProperties({
      sortedByPublish: false,
      sortedByCreated: false,
      sortedByAlpha: false
    });

    switch (this.get('sortProperties.firstObject')) {
    case 'data.publish_date':
      this.set('sortedByPublish', true);
      break;
    case 'data.create_date':
      this.set('sortedByCreated', true);
      break;
    case 'data.name':
      this.set('sortedByAlpha', true);
      break;
    }

  }.observes('sortProperties'),

  cmsItems: Ember.arrayComputed('model.@each.data', 'cmsControls.@each.showInCms', {
    addedItem: function (array, item, changeMeta) {

      if (item.constructor.typeKey === 'control') {
        array.forEach(this._updateItemControls.bind(this));
      } else {
        array.pushObject(this._updateItemControls(item));
      }

      return array;
    },
    removedItem: function (array, item) {
      if (item.constructor.typeKey !== 'control') {
        array.removeObject(item);
      }
      return array;
    }
  }),

  sortedCmsItems: function () {

    var filterQuery = this.get('filterQuery');

    var sortedCmsItems = this.get('cmsItems').sortBy.apply(this, this.get('sortProperties'));

    if (!this.get('sortAscending')) {
      sortedCmsItems.reverse();
    }

    sortedCmsItems = sortedCmsItems.filter(function (item) {
      if (!filterQuery) {
        return true;
      } else {
        return (new RegExp(filterQuery, 'ig')).test(item.get('data.name'));
      }
    });

    return sortedCmsItems;
  }.property('cmsItems.@each', 'sortProperties', 'sortAscending', 'filterQuery'),

  locksChanged: function () {
    this.get('cmsItems').setEach('lockedBy', null);
    this.get('lockedItems').forEach(function (lock) {
      this.get('cmsItems').findBy('id', lock.get('id')).set('lockedBy', lock.get('email'));
    }, this);
  }.observes('lockedItems.@each'),

  actions: {
    deleteItem: function (item) {
      if (window.confirm('Are you sure you want to remove ' + item.get('data.name') + '?')) {
        var id = item.get('id');
        item.destroyRecord().then(function () {
          window.ENV.sendBuildSignal();
          window.ENV.deleteIndex(id, this.get('contentType.id'));
          this.send('notify', 'success', 'Item removed!');
        }.bind(this));
      }
    },

    toggleShowInCms: function (control) {
      control.toggleProperty('showInCms');
      this.get('contentType').save();
    },

    sortToggle: function (field) {

      field = 'data.' + field;

      var sortProperties = this.get('sortProperties');

      if (sortProperties.get('firstObject') === field) {
        this.toggleProperty('sortAscending');
      } else {
        this.set('sortAscending', true);
      }

      sortProperties.insertAt(0, field);
      sortProperties = sortProperties.uniq();

      this.set('sortProperties', sortProperties);

    },

    gotoEdit: function (contentTypeId, itemId) {
      this.transitionToRoute('wh.content.type.edit', contentTypeId, itemId);
    }
  }

});
<|endoftext|>