<commit_before>const _ = require('lodash');
const Promise = require('bluebird');
const tpl = require('@tryghost/tpl');
const errors = require('@tryghost/errors');
const {sequence} = require('@tryghost/promise');

const messages = {
    noUserFound: 'No user found',
    postNotFound: 'Post not found.',
    notEnoughPermission: 'You do not have permission to perform this action'
};

/**
 * Why and when do we have to fetch `authors` by default?
 *
 * # CASE 1
 * We fetch the `authors` relations when you either request `withRelated=['authors']` or `withRelated=['author`].
 * The old `author` relation was removed, but we still have to support this case.
 *
 * ---
 *
 * It's impossible to implement a default `withRelated` feature nicely at the moment, because we can't hook into bookshelf
 * and support all model queries and collection queries (e.g. fetchAll). The hardest part is to remember
 * if the user requested the `authors` or not. Overriding `sync` does not work for collections.
 * And overriding the sync method of Collection does not trigger sync - probably a bookshelf bug, i have
 * not investigated.
 *
 * That's why we remember `_originalOptions` for now - only specific to posts.
 *
 * NOTE: If we fetch the multiple authors manually on the events, we run into the same problem. We have to remember
 * the original options. Plus: we would fetch the authors twice in some cases.
 */
module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
    const proto = Post.prototype;

    const Model = Post.extend({
        _handleOptions: function _handleOptions(fnName) {
            const self = this;

            return function innerHandleOptions(model, attrs, options) {
                model._originalOptions = _.cloneDeep(_.pick(options, ['withRelated']));

                if (!options.withRelated) {
                    options.withRelated = [];
                }

                if (options.withRelated.indexOf('author') !== -1) {
                    options.withRelated.splice(options.withRelated.indexOf('author'), 1);
                    options.withRelated.push('authors');
                }

                if (options.forUpdate &&
                    ['onFetching', 'onFetchingCollection'].indexOf(fnName) !== -1 &&
                    options.withRelated.indexOf('authors') === -1) {
                    options.withRelated.push('authors');
                }

                return proto[fnName].call(self, model, attrs, options);
            };
        },

        onFetching: function onFetching(model, attrs, options) {
            return this._handleOptions('onFetching')(model, attrs, options);
        },

        onFetchingCollection: function onFetchingCollection(collection, attrs, options) {
            return this._handleOptions('onFetchingCollection')(collection, attrs, options);
        },

        onFetchedCollection: function (collection, attrs, options) {
            _.each(collection.models, ((model) => {
                model._originalOptions = collection._originalOptions;
            }));

            return proto.onFetchingCollection.call(this, collection, attrs, options);
        },

        onCreating: function onCreating(model, attrs, options) {
            if (!model.get('authors')) {
                model.set('authors', [{
                    id: this.contextUser(options)
                }]);
            }

            return this._handleOptions('onCreating')(model, attrs, options);
        },

        onUpdating: function onUpdating(model, attrs, options) {
            return this._handleOptions('onUpdating')(model, attrs, options);
        },

        // @NOTE: `post.author` was always ignored [unsupported]
        // @NOTE: triggered before creating and before updating
        onSaving: function (model, attrs, options) {
            const ops = [];

            /**
             * @deprecated: single authors was superceded by multiple authors in Ghost 1.22.0 - `author`, is unused in Ghost 3.0
             */
            model.unset('author');

            // CASE: you can't delete all authors
            if (model.get('authors') && !model.get('authors').length) {
                throw new errors.ValidationError({
                    message: 'At least one author is required.'
                });
            }

            /**
             * @NOTE:
             *
             * Try to find a user with either id, slug or email if "authors" is present.
             * Otherwise fallback to owner user.
             *
             * You cannot create an author via posts!
             * Ghost uses the invite flow to create users.
             */
            if (model.get('authors')) {
                ops.push(() => {
                    return this.matchAuthors(model, options);
                });
            }

            ops.push(() => {
                return proto.onSaving.call(this, model, attrs, options);
            });

            return sequence(ops);
        },

        serialize: function serialize(options) {
            let attrs = proto.serialize.call(this, options);

            // CASE: e.g. you stub model response in the test
            // CASE: you delete a model without fetching before
            if (!this._originalOptions) {
                this._originalOptions = {};
            }

            // CASE: `posts.authors` was not requested, but fetched in specific cases (see top)
            if (!this._originalOptions || !this._originalOptions.withRelated || this._originalOptions.withRelated.indexOf('authors') === -1) {
                delete attrs.authors;
            }

            // If the current column settings allow it...
            if (!options.columns || (options.columns && options.columns.indexOf('primary_author') > -1)) {
                // ... attach a computed property of primary_author which is the first author
                if (attrs.authors && attrs.authors.length) {
                    attrs.primary_author = attrs.authors[0];
                } else {
                    attrs.primary_author = null;
                }
            }

            return attrs;
        },

        /**
         * Authors relation is special. You cannot add new authors via relations.
         * But you can for the tags relation. That's why we have to sort this out before
         * we trigger bookshelf-relations.
         *
         * @TODO: Add a feature to bookshelf-relations to configure if relations can be added or should be matched only.
         */
        matchAuthors(model, options) {
            let ownerUser;
            const ops = [];

            ops.push(() => {
                return ghostBookshelf
                    .model('User')
                    .getOwnerUser(Object.assign({}, _.pick(options, 'transacting')))
                    .then((_ownerUser) => {
                        ownerUser = _ownerUser;
                    });
            });

            ops.push(() => {
                const authors = model.get('authors');
                const authorsToSet = [];

                return Promise.each(authors, (author, index) => {
                    const query = {};

                    if (author.id) {
                        query.id = author.id;
                    } else if (author.slug) {
                        query.slug = author.slug;
                    } else if (author.email) {
                        query.email = author.email;
                    }

                    return ghostBookshelf
                        .model('User')
                        .where(query)
                        .fetch(Object.assign({columns: ['id']}, _.pick(options, 'transacting')))
                        .then((user) => {
                            let userId = user ? user.id : ownerUser.id;

                            // CASE: avoid attaching duplicate authors relation
                            const userExists = _.find(authorsToSet, {id: userId.id});

                            if (!userExists) {
                                authorsToSet[index] = {};
                                authorsToSet[index].id = userId;
                            }
                        });
                }).then(() => {
                    model.set('authors', authorsToSet);
                });
            });

            return sequence(ops);
        }
    }, {
        /**
         * ### reassignByAuthor
         * @param  {Object} unfilteredOptions has context and id. Context is the user doing the destroy, id is the user to destroy
         * @param {string} unfilteredOptions.id
         * @param {Object} unfilteredOptions.context
         * @param {Object} unfilteredOptions.transacting
         */
        reassignByAuthor: async function reassignByAuthor(unfilteredOptions) {
            let options = this.filterOptions(unfilteredOptions, 'reassignByAuthor', {extraAllowedProperties: ['id']});
            let authorId = options.id;

            if (!authorId) {
                return Promise.reject(new errors.NotFoundError({
                    message: tpl(messages.noUserFound)
                }));
            }

            const reassignPost = (async () => {
                let trx = options.transacting;
                let knex = ghostBookshelf.knex;

                try {
                    // There's only one possible owner per Ghost instance
                    const ownerUser = await knex('roles')
                        .transacting(trx)
                        .join('roles_users', 'roles.id', '=', 'roles_users.role_id')
                        .where('roles.name', 'Owner')
                        .select('roles_users.user_id');
                    const ownerId = ownerUser[0].user_id;

                    const authorsPosts = await knex('posts_authors')
                        .transacting(trx)
                        .where('author_id', authorId)
                        .select('post_id', 'sort_order');

                    const ownersPosts = await knex('posts_authors')
                        .transacting(trx)
                        .where('author_id', ownerId)
                        .select('post_id');

                    const authorsPrimaryPosts = authorsPosts.filter(ap => ap.sort_order === 0);
                    const primaryPostsWithOwnerCoauthor = _.intersectionBy(authorsPrimaryPosts, ownersPosts, 'post_id');
                    const primaryPostsWithOwnerCoauthorIds = primaryPostsWithOwnerCoauthor.map(post => post.post_id);

                    // remove author and bump owner's sort_order to 0 to make them a primary author
                    // remove author from posts
                    await knex('posts_authors')
                        .transacting(trx)
                        .whereIn('post_id', primaryPostsWithOwnerCoauthorIds)
                        .where('author_id', authorId)
                        .del();

                    // make the owner a primary author
                    await knex('posts_authors')
                        .transacting(trx)
                        .whereIn('post_id', primaryPostsWithOwnerCoauthorIds)
                        .where('author_id', ownerId)
                        .update('sort_order', 0);

                    const primaryPostsWithoutOwnerCoauthor = _.differenceBy(authorsPrimaryPosts, primaryPostsWithOwnerCoauthor, 'post_id');
                    const postsWithoutOwnerCoauthorIds = primaryPostsWithoutOwnerCoauthor.map(post => post.post_id);

                    // swap out current author with the owner
                    await knex('posts_authors')
                        .transacting(trx)
                        .whereIn('post_id', postsWithoutOwnerCoauthorIds)
                        .where('author_id', authorId)
                        .update('author_id', ownerId);

                    // remove author as secondary author from any other posts
                    await knex('posts_authors')
                        .transacting(trx)
                        .where('author_id', authorId)
                        .del();
                } catch (err) {
                    throw new errors.InternalServerError({err: err});
                }
            });

            if (!options.transacting) {
                return ghostBookshelf.transaction((transacting) => {
                    options.transacting = transacting;
                    return reassignPost();
                });
            }

            return reassignPost();
        },

        permissible: function permissible(postModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) {
            const self = this;
            const postModel = postModelOrId;
            let origArgs;
            let isContributor;
            let isAuthor;
            let isEdit;
            let isAdd;
            let isDestroy;

            // If we passed in an id instead of a model, get the model
            // then check the permissions
            if (_.isNumber(postModelOrId) || _.isString(postModelOrId)) {
                // Grab the original args without the first one
                origArgs = _.toArray(arguments).slice(1);

                // Get the actual post model
                return this.findOne({id: postModelOrId, status: 'all'}, {withRelated: ['authors']})
                    .then(function then(foundPostModel) {
                        if (!foundPostModel) {
                            throw new errors.NotFoundError({
                                message: tpl(messages.postNotFound)
                            });
                        }

                        // Build up the original args but substitute with actual model
                        const newArgs = [foundPostModel].concat(origArgs);
                        return self.permissible.apply(self, newArgs);
                    });
            }

            isContributor = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Contributor'});
            isAuthor = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Author'});
            isEdit = (action === 'edit');
            isAdd = (action === 'add');
            isDestroy = (action === 'destroy');

            function isChangingAuthors() {
                if (!unsafeAttrs.authors) {
                    return false;
                }

                if (!unsafeAttrs.authors.length) {
                    return true;
                }

                return unsafeAttrs.authors[0].id !== postModel.related('authors').models[0].id;
            }

            function isOwner() {
                let isCorrectOwner = true;

                if (!unsafeAttrs.authors) {
                    return false;
                }

                if (unsafeAttrs.authors) {
                    isCorrectOwner = isCorrectOwner && unsafeAttrs.authors.length && unsafeAttrs.authors[0].id === context.user;
                }

                return isCorrectOwner;
            }

            function isPrimaryAuthor() {
                return (context.user === postModel.related('authors').models[0].id);
            }

            function isCoAuthor() {
                return postModel.related('authors').models.map(author => author.id).includes(context.user);
            }

            if (isContributor && isEdit) {
                hasUserPermission = !isChangingAuthors() && isCoAuthor();
            } else if (isContributor && isAdd) {
                hasUserPermission = isOwner();
            } else if (isContributor && isDestroy) {
                hasUserPermission = isPrimaryAuthor();
            } else if (isAuthor && isEdit) {
                hasUserPermission = isCoAuthor() && !isChangingAuthors();
            } else if (isAuthor && isAdd) {
                hasUserPermission = isOwner();
            } else if (postModel) {
                hasUserPermission = hasUserPermission || isPrimaryAuthor();
            }

            if (hasUserPermission && hasApiKeyPermission) {
                return Post.permissible.call(
                    this,
                    postModelOrId,
                    action, context,
                    unsafeAttrs,
                    loadedPermissions,
                    hasUserPermission,
                    hasApiKeyPermission
                ).then(({excludedAttrs}) => {
                    // @TODO: we need a concept for making a diff between incoming authors and existing authors
                    // @TODO: for now we simply re-use the new concept of `excludedAttrs`
                    // We only check the primary author of `authors`, any other change will be ignored.
                    if (isContributor || isAuthor) {
                        return {
                            excludedAttrs: ['authors'].concat(excludedAttrs)
                        };
                    }
                    return {excludedAttrs};
                });
            }

            return Promise.reject(new errors.NoPermissionError({
                message: tpl(messages.notEnoughPermission)
            }));
        }
    });

    return Model;
};
<commit_msg>Replaced Promise.each() with .all() in models/relations/author (#15614)<commit_after>const _ = require('lodash');
const tpl = require('@tryghost/tpl');
const errors = require('@tryghost/errors');
const {sequence} = require('@tryghost/promise');

const messages = {
    noUserFound: 'No user found',
    postNotFound: 'Post not found.',
    notEnoughPermission: 'You do not have permission to perform this action'
};

/**
 * Why and when do we have to fetch `authors` by default?
 *
 * # CASE 1
 * We fetch the `authors` relations when you either request `withRelated=['authors']` or `withRelated=['author`].
 * The old `author` relation was removed, but we still have to support this case.
 *
 * ---
 *
 * It's impossible to implement a default `withRelated` feature nicely at the moment, because we can't hook into bookshelf
 * and support all model queries and collection queries (e.g. fetchAll). The hardest part is to remember
 * if the user requested the `authors` or not. Overriding `sync` does not work for collections.
 * And overriding the sync method of Collection does not trigger sync - probably a bookshelf bug, i have
 * not investigated.
 *
 * That's why we remember `_originalOptions` for now - only specific to posts.
 *
 * NOTE: If we fetch the multiple authors manually on the events, we run into the same problem. We have to remember
 * the original options. Plus: we would fetch the authors twice in some cases.
 */
module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
    const proto = Post.prototype;

    const Model = Post.extend({
        _handleOptions: function _handleOptions(fnName) {
            const self = this;

            return function innerHandleOptions(model, attrs, options) {
                model._originalOptions = _.cloneDeep(_.pick(options, ['withRelated']));

                if (!options.withRelated) {
                    options.withRelated = [];
                }

                if (options.withRelated.indexOf('author') !== -1) {
                    options.withRelated.splice(options.withRelated.indexOf('author'), 1);
                    options.withRelated.push('authors');
                }

                if (options.forUpdate &&
                    ['onFetching', 'onFetchingCollection'].indexOf(fnName) !== -1 &&
                    options.withRelated.indexOf('authors') === -1) {
                    options.withRelated.push('authors');
                }

                return proto[fnName].call(self, model, attrs, options);
            };
        },

        onFetching: function onFetching(model, attrs, options) {
            return this._handleOptions('onFetching')(model, attrs, options);
        },

        onFetchingCollection: function onFetchingCollection(collection, attrs, options) {
            return this._handleOptions('onFetchingCollection')(collection, attrs, options);
        },

        onFetchedCollection: function (collection, attrs, options) {
            _.each(collection.models, ((model) => {
                model._originalOptions = collection._originalOptions;
            }));

            return proto.onFetchingCollection.call(this, collection, attrs, options);
        },

        onCreating: function onCreating(model, attrs, options) {
            if (!model.get('authors')) {
                model.set('authors', [{
                    id: this.contextUser(options)
                }]);
            }

            return this._handleOptions('onCreating')(model, attrs, options);
        },

        onUpdating: function onUpdating(model, attrs, options) {
            return this._handleOptions('onUpdating')(model, attrs, options);
        },

        // @NOTE: `post.author` was always ignored [unsupported]
        // @NOTE: triggered before creating and before updating
        onSaving: function (model, attrs, options) {
            const ops = [];

            /**
             * @deprecated: single authors was superceded by multiple authors in Ghost 1.22.0 - `author`, is unused in Ghost 3.0
             */
            model.unset('author');

            // CASE: you can't delete all authors
            if (model.get('authors') && !model.get('authors').length) {
                throw new errors.ValidationError({
                    message: 'At least one author is required.'
                });
            }

            /**
             * @NOTE:
             *
             * Try to find a user with either id, slug or email if "authors" is present.
             * Otherwise fallback to owner user.
             *
             * You cannot create an author via posts!
             * Ghost uses the invite flow to create users.
             */
            if (model.get('authors')) {
                ops.push(() => {
                    return this.matchAuthors(model, options);
                });
            }

            ops.push(() => {
                return proto.onSaving.call(this, model, attrs, options);
            });

            return sequence(ops);
        },

        serialize: function serialize(options) {
            let attrs = proto.serialize.call(this, options);

            // CASE: e.g. you stub model response in the test
            // CASE: you delete a model without fetching before
            if (!this._originalOptions) {
                this._originalOptions = {};
            }

            // CASE: `posts.authors` was not requested, but fetched in specific cases (see top)
            if (!this._originalOptions || !this._originalOptions.withRelated || this._originalOptions.withRelated.indexOf('authors') === -1) {
                delete attrs.authors;
            }

            // If the current column settings allow it...
            if (!options.columns || (options.columns && options.columns.indexOf('primary_author') > -1)) {
                // ... attach a computed property of primary_author which is the first author
                if (attrs.authors && attrs.authors.length) {
                    attrs.primary_author = attrs.authors[0];
                } else {
                    attrs.primary_author = null;
                }
            }

            return attrs;
        },

        /**
         * Authors relation is special. You cannot add new authors via relations.
         * But you can for the tags relation. That's why we have to sort this out before
         * we trigger bookshelf-relations.
         *
         * @TODO: Add a feature to bookshelf-relations to configure if relations can be added or should be matched only.
         */
        matchAuthors(model, options) {
            let ownerUser;
            const ops = [];

            ops.push(() => {
                return ghostBookshelf
                    .model('User')
                    .getOwnerUser(Object.assign({}, _.pick(options, 'transacting')))
                    .then((_ownerUser) => {
                        ownerUser = _ownerUser;
                    });
            });

            ops.push(() => {
                const authors = model.get('authors');
                const authorsToSet = [];

                return Promise.all(authors.map((author, index) => {
                    const query = {};

                    if (author.id) {
                        query.id = author.id;
                    } else if (author.slug) {
                        query.slug = author.slug;
                    } else if (author.email) {
                        query.email = author.email;
                    }

                    return ghostBookshelf
                        .model('User')
                        .where(query)
                        .fetch(Object.assign({columns: ['id']}, _.pick(options, 'transacting')))
                        .then((user) => {
                            let userId = user ? user.id : ownerUser.id;

                            // CASE: avoid attaching duplicate authors relation
                            const userExists = _.find(authorsToSet, {id: userId.id});

                            if (!userExists) {
                                authorsToSet[index] = {};
                                authorsToSet[index].id = userId;
                            }
                        });
                })).then(() => {
                    model.set('authors', authorsToSet);
                });
            });

            return sequence(ops);
        }
    }, {
        /**
         * ### reassignByAuthor
         * @param  {Object} unfilteredOptions has context and id. Context is the user doing the destroy, id is the user to destroy
         * @param {string} unfilteredOptions.id
         * @param {Object} unfilteredOptions.context
         * @param {Object} unfilteredOptions.transacting
         */
        reassignByAuthor: async function reassignByAuthor(unfilteredOptions) {
            let options = this.filterOptions(unfilteredOptions, 'reassignByAuthor', {extraAllowedProperties: ['id']});
            let authorId = options.id;

            if (!authorId) {
                return Promise.reject(new errors.NotFoundError({
                    message: tpl(messages.noUserFound)
                }));
            }

            const reassignPost = (async () => {
                let trx = options.transacting;
                let knex = ghostBookshelf.knex;

                try {
                    // There's only one possible owner per Ghost instance
                    const ownerUser = await knex('roles')
                        .transacting(trx)
                        .join('roles_users', 'roles.id', '=', 'roles_users.role_id')
                        .where('roles.name', 'Owner')
                        .select('roles_users.user_id');
                    const ownerId = ownerUser[0].user_id;

                    const authorsPosts = await knex('posts_authors')
                        .transacting(trx)
                        .where('author_id', authorId)
                        .select('post_id', 'sort_order');

                    const ownersPosts = await knex('posts_authors')
                        .transacting(trx)
                        .where('author_id', ownerId)
                        .select('post_id');

                    const authorsPrimaryPosts = authorsPosts.filter(ap => ap.sort_order === 0);
                    const primaryPostsWithOwnerCoauthor = _.intersectionBy(authorsPrimaryPosts, ownersPosts, 'post_id');
                    const primaryPostsWithOwnerCoauthorIds = primaryPostsWithOwnerCoauthor.map(post => post.post_id);

                    // remove author and bump owner's sort_order to 0 to make them a primary author
                    // remove author from posts
                    await knex('posts_authors')
                        .transacting(trx)
                        .whereIn('post_id', primaryPostsWithOwnerCoauthorIds)
                        .where('author_id', authorId)
                        .del();

                    // make the owner a primary author
                    await knex('posts_authors')
                        .transacting(trx)
                        .whereIn('post_id', primaryPostsWithOwnerCoauthorIds)
                        .where('author_id', ownerId)
                        .update('sort_order', 0);

                    const primaryPostsWithoutOwnerCoauthor = _.differenceBy(authorsPrimaryPosts, primaryPostsWithOwnerCoauthor, 'post_id');
                    const postsWithoutOwnerCoauthorIds = primaryPostsWithoutOwnerCoauthor.map(post => post.post_id);

                    // swap out current author with the owner
                    await knex('posts_authors')
                        .transacting(trx)
                        .whereIn('post_id', postsWithoutOwnerCoauthorIds)
                        .where('author_id', authorId)
                        .update('author_id', ownerId);

                    // remove author as secondary author from any other posts
                    await knex('posts_authors')
                        .transacting(trx)
                        .where('author_id', authorId)
                        .del();
                } catch (err) {
                    throw new errors.InternalServerError({err: err});
                }
            });

            if (!options.transacting) {
                return ghostBookshelf.transaction((transacting) => {
                    options.transacting = transacting;
                    return reassignPost();
                });
            }

            return reassignPost();
        },

        permissible: function permissible(postModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) {
            const self = this;
            const postModel = postModelOrId;
            let origArgs;
            let isContributor;
            let isAuthor;
            let isEdit;
            let isAdd;
            let isDestroy;

            // If we passed in an id instead of a model, get the model
            // then check the permissions
            if (_.isNumber(postModelOrId) || _.isString(postModelOrId)) {
                // Grab the original args without the first one
                origArgs = _.toArray(arguments).slice(1);

                // Get the actual post model
                return this.findOne({id: postModelOrId, status: 'all'}, {withRelated: ['authors']})
                    .then(function then(foundPostModel) {
                        if (!foundPostModel) {
                            throw new errors.NotFoundError({
                                message: tpl(messages.postNotFound)
                            });
                        }

                        // Build up the original args but substitute with actual model
                        const newArgs = [foundPostModel].concat(origArgs);
                        return self.permissible.apply(self, newArgs);
                    });
            }

            isContributor = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Contributor'});
            isAuthor = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Author'});
            isEdit = (action === 'edit');
            isAdd = (action === 'add');
            isDestroy = (action === 'destroy');

            function isChangingAuthors() {
                if (!unsafeAttrs.authors) {
                    return false;
                }

                if (!unsafeAttrs.authors.length) {
                    return true;
                }

                return unsafeAttrs.authors[0].id !== postModel.related('authors').models[0].id;
            }

            function isOwner() {
                let isCorrectOwner = true;

                if (!unsafeAttrs.authors) {
                    return false;
                }

                if (unsafeAttrs.authors) {
                    isCorrectOwner = isCorrectOwner && unsafeAttrs.authors.length && unsafeAttrs.authors[0].id === context.user;
                }

                return isCorrectOwner;
            }

            function isPrimaryAuthor() {
                return (context.user === postModel.related('authors').models[0].id);
            }

            function isCoAuthor() {
                return postModel.related('authors').models.map(author => author.id).includes(context.user);
            }

            if (isContributor && isEdit) {
                hasUserPermission = !isChangingAuthors() && isCoAuthor();
            } else if (isContributor && isAdd) {
                hasUserPermission = isOwner();
            } else if (isContributor && isDestroy) {
                hasUserPermission = isPrimaryAuthor();
            } else if (isAuthor && isEdit) {
                hasUserPermission = isCoAuthor() && !isChangingAuthors();
            } else if (isAuthor && isAdd) {
                hasUserPermission = isOwner();
            } else if (postModel) {
                hasUserPermission = hasUserPermission || isPrimaryAuthor();
            }

            if (hasUserPermission && hasApiKeyPermission) {
                return Post.permissible.call(
                    this,
                    postModelOrId,
                    action, context,
                    unsafeAttrs,
                    loadedPermissions,
                    hasUserPermission,
                    hasApiKeyPermission
                ).then(({excludedAttrs}) => {
                    // @TODO: we need a concept for making a diff between incoming authors and existing authors
                    // @TODO: for now we simply re-use the new concept of `excludedAttrs`
                    // We only check the primary author of `authors`, any other change will be ignored.
                    if (isContributor || isAuthor) {
                        return {
                            excludedAttrs: ['authors'].concat(excludedAttrs)
                        };
                    }
                    return {excludedAttrs};
                });
            }

            return Promise.reject(new errors.NoPermissionError({
                message: tpl(messages.notEnoughPermission)
            }));
        }
    });

    return Model;
};
<|endoftext|>


<commit_before>'use strict';

/* Copyright 2014 Azure Standard https://www.azurestandard.com/
 * Released under the MIT license (http://opensource.org/licenses/MIT).
 *
 * A WebSocket service for AngularJS
 */

var websocket = angular.module('websocket', []);

websocket.
    factory('websocket', ['$rootScope', function($rootScope) {
        var ws, make_message, parse_message, dispatch, Service, ready, queue;
        ready = false;
        queue = [];

        //Websocket setup
        ws = new window.WebSocket("ws://automation.azurestandard.com:9000");

        ws.onopen = function () {
            console.log("Socket has been opened");
            ready = true;
            if (queue.length) {
                queue.forEach(function (item) {
                    this.send(item);
                }, ws);
            }
            queue = [];
        }

        ws.onerror = function (error) {
            console.log("WebSocket Error ");
            console.log(error);
        };

        // Log messages from the server
        ws.onmessage = function (msg) {
            var parsed;
            parsed = parse_message(msg.data);
            //This is a 'service' level message, which all service
            //consumers should listen to, and react by reloading.
            if (parsed.topic == '/refresh') {
                window.location.reload();
            }
            dispatch.handle(parsed.topic, parsed.body)
            $rootScope.$apply();
        };

        //dispatch
        dispatch = {
            //TODO underscore has handy functions to treat arrays as
            //sets. Maybe clearer then this.
            listeners: {},
            register: function (topic, func) {
                var current;
                topic = topic.toLowerCase();
                current = this.listeners[topic] || [];
                current.push(func);
                this.listeners[topic] = _.unique(current, false, toString);
            },
            deRegister: function (topic, func) {
                this.listeners['topic'] = _.reject(
                    this.listeners['topic'], function (el) {
                        return el.toString() === func.toString();
                    });
            },
            handle: function (topic, body) {
                var key, interested;
                interested = [];
                _.each(_.keys(this.listeners),
                       function (key) {
                           if (topic.indexOf(key) === 0) {
                               interested.push(this.listeners[key])
                           }
                       },
                       this
                      );
                _.map(_.unique(_.flatten(interested), false, toString),
                      function (item) {
                          item(topic, _.extend(body));
                      });
            }
        }

        // Utilities
        make_message = function (topic, body) {
            return topic + " " + JSON.stringify(body);
        };

        parse_message = function (msg) {
            var topic, body, parts;
            parts = msg.split(" ", 1);
            topic = parts[0];
            body = JSON.parse(msg.substring(topic.length + 1));
            return {"topic": topic, "body": body};
        };

        var send = function (msg) {
            if (ready) {
                ws.send(msg);
            } else {
                queue.push(msg);
            }
        };

        // We return this object to anything injecting our service
        Service = {
            emit: function (topic, body) {
                this.send(make_message(topic,body));
            },

            register: function (topic, func) {
                dispatch.register(topic, func);
            },
        }

        return Service;
    }]);
<commit_msg>websocket.js: Remove some empty lines and outdated comments<commit_after>'use strict';

/* Copyright 2014 Azure Standard https://www.azurestandard.com/
 * Released under the MIT license (http://opensource.org/licenses/MIT).
 *
 * A WebSocket service for AngularJS
 */

var websocket = angular.module('websocket', []);

websocket.
    factory('websocket', ['$rootScope', function($rootScope) {
        var ws, make_message, parse_message, dispatch, Service, ready, queue;
        ready = false;
        queue = [];

        //Websocket setup
        ws = new window.WebSocket("ws://automation.azurestandard.com:9000");

        ws.onopen = function () {
            console.log("Socket has been opened");
            ready = true;
            if (queue.length) {
                queue.forEach(function (item) {
                    this.send(item);
                }, ws);
            }
            queue = [];
        }

        ws.onerror = function (error) {
            console.log("WebSocket Error ");
            console.log(error);
        };

        // Log messages from the server
        ws.onmessage = function (msg) {
            var parsed;
            parsed = parse_message(msg.data);
            //This is a 'service' level message, which all service
            //consumers should listen to, and react by reloading.
            if (parsed.topic == '/refresh') {
                window.location.reload();
            }
            dispatch.handle(parsed.topic, parsed.body)
            $rootScope.$apply();
        };

        dispatch = {
            listeners: {},
            register: function (topic, func) {
                var current;
                topic = topic.toLowerCase();
                current = this.listeners[topic] || [];
                current.push(func);
                this.listeners[topic] = _.unique(current, false, toString);
            },
            deRegister: function (topic, func) {
                this.listeners['topic'] = _.reject(
                    this.listeners['topic'], function (el) {
                        return el.toString() === func.toString();
                    });
            },
            handle: function (topic, body) {
                var key, interested;
                interested = [];
                _.each(_.keys(this.listeners),
                       function (key) {
                           if (topic.indexOf(key) === 0) {
                               interested.push(this.listeners[key])
                           }
                       },
                       this
                      );
                _.map(_.unique(_.flatten(interested), false, toString),
                      function (item) {
                          item(topic, _.extend(body));
                      });
            }
        }

        // Utilities
        make_message = function (topic, body) {
            return topic + " " + JSON.stringify(body);
        };

        parse_message = function (msg) {
            var topic, body, parts;
            parts = msg.split(" ", 1);
            topic = parts[0];
            body = JSON.parse(msg.substring(topic.length + 1));
            return {"topic": topic, "body": body};
        };

        var send = function (msg) {
            if (ready) {
                ws.send(msg);
            } else {
                queue.push(msg);
            }
        };

        // We return this object to anything injecting our service
        Service = {
            emit: function (topic, body) {
                this.send(make_message(topic,body));
            },

            register: function (topic, func) {
                dispatch.register(topic, func);
            },
        }

        return Service;
    }]);
<|endoftext|>


<commit_before>import postcss from 'postcss';
import test from 'ava';
import plugin from '../src';

const processing = (input, opts) => {
    return postcss([plugin(opts)]).process(input).css;
};

test('it change circular reference', t => {
    const expected = ':root{ --from: 1; --to: 1}';
    const value = ':root{ --from: 1; --to: var(--from)}';
    t.is(processing(value), expected);
});

test('it change first properties for @for', t => {
    const expected = ':root{ --from: 1; } @for $i from 1 to 2';
    const value = ':root{ --from: 1; } @for $i from var(--from) to 2';
    t.is(processing(value), expected);
});

test('it change second properties for @for', t => {
    const expected = ':root{ --to: 2; } @for $i from 1 to 2';
    const value = ':root{ --to: 2; } @for $i from 1 to var(--to)';
    t.is(processing(value), expected);
});

test('it change two properties for @for', t => {
    const expected = ':root{ --from: 1; --to: 2; } @for $i from 1 to 2';
    const value = ':root{ --from: 1; --to: 2; } @for $i from var(--from) to var(--to)';
    t.is(processing(value), expected);
});

test('it change three properties for @for', t => {
    const expected = ':root{ --from: 1; --to: 2; --step: 5 } @for $i from 1 to 2 by 5';
    const value = ':root{ --from: 1; --to: 2; --step: 5 } @for $i from var(--from) to var(--to) by var(--step)';
    t.is(processing(value), expected);
});

test('it change two properties for @if', t => {
    const expected = ':root{ --first: 1; --second: 2; } @if 1 < 2';
    const value = ':root{ --first: 1; --second: 2; } @if var(--first) < var(--second)';
    t.is(processing(value, {atRules: ['if']}), expected);
});

test('it change two properties for @if, @else if', t => {
    const expected = ':root{ --first: 1; --second: 2; } @if 1 < 2 { color: olive; } @else if 1 > 2 { color: red; }';
    const value = ':root{ --first: 1; --second: 2; } @if var(--first) < var(--second) { color: olive; } @else if var(--first) > var(--second) { color: red; }';
    t.is(processing(value, {atRules: ['if', 'else']}), expected);
});

test('it change multi properties for @each', t => {
    const expected = ':root{ --array: foo, bar, baz; } @each $val in foo, bar, baz {} @for foo, bar, baz {}';
    const value = ':root{ --array: foo, bar, baz; } @each $val in var(--array) {} @for var(--array) {}';
    t.is(processing(value, {atRules: ['each']}), expected);
});

test('it without variables', t => {
    const expected = ':root{ --red: red; } @if var(--green) { color: var(--green) }';
    const value = ':root{ --red: red; } @if var(--green) { color: var(--green) }';
    t.is(processing(value), expected);
});

test('chould change from options variables', t => {
    const expected = '@if green { .text-green { color: var(--green) }}';
    const value = '@if var(--green) { .text-green { color: var(--green) }}';
    const variables = {
        '--green': 'green'
    };
    t.is(processing(value, {variables: variables}), expected);
});

test('should change for @custom-media', t => {
    const expected = ':root{ --breakpoint-xs: 29.25em } @custom-media --viewport-xs (width > 29.25em)';
    const value = ':root{ --breakpoint-xs: 29.25em } @custom-media --viewport-xs (width > var(--breakpoint-xs))';
    t.is(processing(value, {atRules: ['custom-media']}), expected);
});
<commit_msg>test: fix test for issue #181, pkg should not change source<commit_after>import postcss from 'postcss';
import test from 'ava';
import plugin from '../src';

const processing = (input, opts) => {
    return postcss([plugin(opts)]).process(input).css;
};

test('it change circular reference', t => {
    const expected = ':root{ --from: 1; --to: var(--from)} @for $i from 1 to 1';
    const value = ':root{ --from: 1; --to: var(--from)} @for $i from var(--from) to var(--to)';
    t.is(processing(value), expected);
});

// test('it change first properties for @for', t => {
//     const expected = ':root{ --from: 1; } @for $i from 1 to 2';
//     const value = ':root{ --from: 1; } @for $i from var(--from) to 2';
//     t.is(processing(value), expected);
// });

// test('it change second properties for @for', t => {
//     const expected = ':root{ --to: 2; } @for $i from 1 to 2';
//     const value = ':root{ --to: 2; } @for $i from 1 to var(--to)';
//     t.is(processing(value), expected);
// });

// test('it change two properties for @for', t => {
//     const expected = ':root{ --from: 1; --to: 2; } @for $i from 1 to 2';
//     const value = ':root{ --from: 1; --to: 2; } @for $i from var(--from) to var(--to)';
//     t.is(processing(value), expected);
// });

// test('it change three properties for @for', t => {
//     const expected = ':root{ --from: 1; --to: 2; --step: 5 } @for $i from 1 to 2 by 5';
//     const value = ':root{ --from: 1; --to: 2; --step: 5 } @for $i from var(--from) to var(--to) by var(--step)';
//     t.is(processing(value), expected);
// });

// test('it change two properties for @if', t => {
//     const expected = ':root{ --first: 1; --second: 2; } @if 1 < 2';
//     const value = ':root{ --first: 1; --second: 2; } @if var(--first) < var(--second)';
//     t.is(processing(value, {atRules: ['if']}), expected);
// });

// test('it change two properties for @if, @else if', t => {
//     const expected = ':root{ --first: 1; --second: 2; } @if 1 < 2 { color: olive; } @else if 1 > 2 { color: red; }';
//     const value = ':root{ --first: 1; --second: 2; } @if var(--first) < var(--second) { color: olive; } @else if var(--first) > var(--second) { color: red; }';
//     t.is(processing(value, {atRules: ['if', 'else']}), expected);
// });

// test('it change multi properties for @each', t => {
//     const expected = ':root{ --array: foo, bar, baz; } @each $val in foo, bar, baz {} @for foo, bar, baz {}';
//     const value = ':root{ --array: foo, bar, baz; } @each $val in var(--array) {} @for var(--array) {}';
//     t.is(processing(value, {atRules: ['each']}), expected);
// });

// test('it without variables', t => {
//     const expected = ':root{ --red: red; } @if var(--green) { color: var(--green) }';
//     const value = ':root{ --red: red; } @if var(--green) { color: var(--green) }';
//     t.is(processing(value), expected);
// });

// test('chould change from options variables', t => {
//     const expected = '@if green { .text-green { color: var(--green) }}';
//     const value = '@if var(--green) { .text-green { color: var(--green) }}';
//     const variables = {
//         '--green': 'green'
//     };
//     t.is(processing(value, {variables: variables}), expected);
// });

// test('should change for @custom-media', t => {
//     const expected = ':root{ --breakpoint-xs: 29.25em } @custom-media --viewport-xs (width > 29.25em)';
//     const value = ':root{ --breakpoint-xs: 29.25em } @custom-media --viewport-xs (width > var(--breakpoint-xs))';
//     t.is(processing(value, {atRules: ['custom-media']}), expected);
// });
<|endoftext|>


<commit_before>/* eslint-disable no-console */
/* global browser: false */

require(`babel-register`);
const {inject} = require(`./tooling/lib/openh264`);
const dotenv = require(`dotenv`);
const glob = require(`glob`);
const path = require(`path`);
const os = require(`os`);
const webpackConfig = require(`./webpack.config`);

dotenv.config({path: `.env.default`});
dotenv.config();

require(`babel-register`)({
  only: [
    `./packages/node_modules/**/*.js`
  ],
  sourceMaps: true
});

const PORT = process.env.PORT || 8000;
const CI = !!(process.env.JENKINS || process.env.CI);

exports.config = {
  //
  // ==================
  // Specify Test Files
  // ==================
  // Define which test specs should run. The pattern is relative to the directory
  // from which `wdio` was called. Notice that, if you are calling `wdio` from an
  // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
  // directory is where your package.json resides, so `wdio` will be called from there.
  //
  specs: [
    `./packages/node_modules/{*,*/*}/test/wdio/spec/**/*.js`
  ],
  suites: glob
    .sync(`**/package.json`, {cwd: `./packages/node_modules`})
    .map((p) => path.dirname(p))
    .reduce((suites, p) => {
      suites[p] = [
        `./packages/node_modules/${p}/test/wdio/spec/**/*.js`
      ];
      return suites;
    }, {}),
  //
  // ============
  // Capabilities
  // ============
  // Define your capabilities here. WebdriverIO can run multiple capabilities at the same
  // time. Depending on the number of capabilities, WebdriverIO launches several test
  // sessions. Within your capabilities you can overwrite the spec and exclude options in
  // order to group specific specs to a specific capability.
  //
  // First, you can define how many instances should be started at the same time. Let's
  // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
  // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
  // files and you set maxInstances to 10, all spec files will get tested at the same time
  // and 30 processes will get spawned. The property handles how many capabilities
  // from the same test should run tests.
  //
  maxInstances: 10,
  //
  // If you have trouble getting all important capabilities together, check out the
  // Sauce Labs platform configurator - a great tool to configure your capabilities:
  // https://docs.saucelabs.com/reference/platforms-configurator
  //
  capabilities: {
    browserSpock: {
      desiredCapabilities: {
        browserName: `firefox`,
        tunnelIdentifier: process.env.SC_TUNNEL_IDENTIFIER
      }
    },
    browserMccoy: {
      desiredCapabilities: {
        browserName: `chrome`,
        chromeOptions: {
          args: [
            `--use-fake-device-for-media-stream`,
            `--use-fake-ui-for-media-stream`
          ]
        },
        tunnelIdentifier: process.env.SC_TUNNEL_IDENTIFIER
      }
    }
  },
  //
  // ===================
  // Test Configurations
  // ===================
  // Define all options that are relevant for the WebdriverIO instance here
  //
  // By default WebdriverIO commands are executed in a synchronous way using
  // the wdio-sync package. If you still want to run your tests in an async way
  // e.g. using promises you can set the sync option to false.
  sync: true,
  //
  // Level of logging verbosity: silent | verbose | command | data | result | error
  logLevel: `error`,
  //
  // Enables colors for log output.
  coloredLogs: true,
  //
  // If you only want to run your tests until a specific amount of tests have failed use
  // bail (default is 0 - don't bail, run all tests).
  bail: 0,
  //
  // Saves a screenshot to a given path if a command fails.
  screenshotPath: `./reports/screenshots/`,
  //
  // Set a base URL in order to shorten url command calls. If your url parameter starts
  // with "/", then the base url gets prepended.
  baseUrl: `http://localhost:${PORT}/packages/node_modules`,
  //
  // Default timeout for all waitFor* commands.
  waitforTimeout: 10000,
  //
  // Default timeout in milliseconds for request
  // if Selenium Grid doesn't send response
  connectionRetryTimeout: 90000,
  //
  // Default request retries count
  connectionRetryCount: 3,
  //
  // Initialize the browser instance with a WebdriverIO plugin. The object should have the
  // plugin name as key and the desired plugin options as properties. Make sure you have
  // the plugin installed before running any tests. The following plugins are currently
  // available:
  // WebdriverCSS: https://github.com/webdriverio/webdrivercss
  // WebdriverRTC: https://github.com/webdriverio/webdriverrtc
  // Browserevent: https://github.com/webdriverio/browserevent
  // plugins: {
  //     webdrivercss: {
  //         screenshotRoot: 'my-shots',
  //         failedComparisonsRoot: 'diffs',
  //         misMatchTolerance: 0.05,
  //         screenWidth: [320,480,640,1024]
  //     },
  //     webdriverrtc: {},
  //     browserevent: {}
  // },
  //
  // Test runner services
  // Services take over a specific job you don't want to take care of. They enhance
  // your test setup with almost no effort. Unlike plugins, they don't add new
  // commands. Instead, they hook themselves up into the test process.
  services: CI ? [
    `sauce`,
    `static-server`,
    `webpack`
  ] : [
    `selenium-standalone`,
    `static-server`,
    `webpack`
  ],
  staticServerFolders: [
    {mount: `/`, path: `.`}
  ],
  staticServerPort: PORT,
  webpackConfig,

  //
  // Framework you want to run your specs with.
  // The following are supported: Mocha, Jasmine, and Cucumber
  // see also: http://webdriver.io/guide/testrunner/frameworks.html
  //
  // Make sure you have the wdio adapter package for the specific framework installed
  // before running any tests.
  framework: `mocha`,
  //
  // Test reporter for stdout.
  // The only one supported by default is 'dot'
  // see also: http://webdriver.io/guide/testrunner/reporters.html
  reporters: CI ? [`spec`, `junit`] : [`spec`],
  reporterOptions: {
    junit: {
      outputDir: `./reports/junit/wdio`
    }
  },
  //
  // Options to be passed to Mocha.
  // See the full list at http://mochajs.org/
  mochaOpts: {
    require: [`mocha-steps`],
    timeout: 40000,
    ui: `bdd`
  },
  //
  // =====
  // Hooks
  // =====
  // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
  // it and to build services around it. You can either apply a single function or an array of
  // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
  // resolved to continue.
  /**
   * Gets executed once before all workers get launched.
   * @param {Object} config wdio configuration object
   * @param {Array.<Object>} capabilities list of capabilities details
   * @returns {Promise}
   */
  onPrepare(config, capabilities) {
    const defs = [
      capabilities.browserSpock.desiredCapabilities,
      capabilities.browserMccoy.desiredCapabilities
    ];

    const build = process.env.BUILD_NUMBER || `local-${process.env.USER}-wdio-${Date.now()}`;
    defs.forEach((d) => {
      if (process.env.CI) {
        d.build = build;
        // Set the base to SauceLabs so that inject() does its thing.
        d.base = `SauceLabs`;

        d.version = d.version || `latest`;
        d.platform = d.platform || `OS X 10.12`;
      }
      else {
        // Copy the base over so that inject() does its thing.
        d.base = d.browserName;
        d.platform = os.platform();
      }
    });

    // The openh264 profile seems to break tests locally; run the tests twice
    // and the plugin should download automatically.
    return CI ? inject(defs) : Promise.resolve()
      .then(() => {
        // Remove the base because it's not actually a selenium property
        defs.forEach((d) => Reflect.deleteProperty(d, `base`));
      });
  },
  /**
   * Gets executed just before initialising the webdriver session and test framework. It allows you
   * to manipulate configurations depending on the capability or spec.
   * @param {Object} config wdio configuration object
   * @param {Array.<Object>} capabilities list of capabilities details
   * @param {Array.<String>} specs List of spec file paths that are to be run
   */
  // beforeSession: function (config, capabilities, specs) {
  // },
  /**
   * Gets executed before test execution begins. At this point you can access to all global
   * variables like `browser`. It is the perfect place to define custom commands.
   * @param {Array.<Object>} capabilities list of capabilities details
   * @param {Array.<String>} specs List of spec file paths that are to be run
   */
  // before(capabilities, specs) {
  // }
  //
  /**
   * Hook that gets executed before the suite starts
   * @param {Object} suite suite details
   */
  // beforeSuite: function (suite) {
  // },
  /**
   * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
   * beforeEach in Mocha)
   */
  // beforeHook: function () {
  // },
  /**
   * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
   * afterEach in Mocha)
   */
  // afterHook: function () {
  // },
  /**
   * Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
   * @param {Object} test test details
   */
  // beforeTest: function (test) {
  // },
  /**
   * Runs before a WebdriverIO command gets executed.
   * @param {String} commandName hook command name
   * @param {Array} args arguments that command would receive
   */
  // beforeCommand: function (commandName, args) {
  // },
  /**
   * Runs after a WebdriverIO command gets executed
   * @param {String} commandName hook command name
   * @param {Array} args arguments that command would receive
   * @param {Number} result 0 - command success, 1 - command error
   * @param {Object} error error object if any
   */
  // afterCommand: function (commandName, args, result, error) {
  // },
  /**
   * Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
   * @param {Object} test test details
   * @returns {undefined}
   */
  afterTest(test) {
    if (!test.passed) {
      const logTypes = browser.logTypes();

      Object.keys(logTypes).forEach((browserId) => {
        console.log(logTypes[browserId].value);
        if (logTypes[browserId].value.includes(`browser`)) {
          const logs = browser.select(browserId).log(`browser`);
          if (logs.value.length) {
            console.error(`Test ${test.fullTitle} failed with the following log output from browser ${browserId}`);
            console.log(logs);
            console.error(logs
              .value
              .map((v) => `> ${v.message}`)
              .join(`\n`));
          }
          else {
            console.error(`Test ${test.fullTitle} failed but no logs were produced by browser ${browserId}`);
          }
        }
        else {
          console.error(`${test.fullTitle} failed but browser ${browserId} doesn't support log collection`);
        }
      });
    }
  }
  /**
   * Hook that gets executed after the suite has ended
   * @param {Object} suite suite details
   */
  // afterSuite: function (suite) {
  // },
  /**
   * Gets executed after all tests are done. You still have access to all global variables from
   * the test.
   * @param {Number} result 0 - test pass, 1 - test fail
   * @param {Array.<Object>} capabilities list of capabilities details
   * @param {Array.<String>} specs List of spec file paths that ran
   */
  // after: function (result, capabilities, specs) {
  // },
  /**
   * Gets executed right after terminating the webdriver session.
   * @param {Object} config wdio configuration object
   * @param {Array.<Object>} capabilities list of capabilities details
   * @param {Array.<String>} specs List of spec file paths that ran
   */
  // afterSession: function (config, capabilities, specs) {
  // },
  /**
   * Gets executed after all workers got shut down and the process is about to exit. It is not
   * possible to defer the end of the process using a promise.
   * @param {Object} exitCode 0 - success, 1 - fail
   */
  // onComplete: function(exitCode) {
  // }
};

if (CI) {
  // I couldn't get multiremote + sauce to work while letting wdio handle the
  // tunnel setup. use `npm run sauce:start` and `npm run sauce:run` to start
  // the tunnel and run tests
  exports.config = Object.assign(exports.config, {
    user: process.env.SAUCE_USERNAME,
    key: process.env.SAUCE_ACCESS_KEY,
    sauceConnect: false
  });
}
<commit_msg>chore(tooling): read env correctly<commit_after>/* eslint-disable no-console */
/* global browser: false */

require(`babel-register`);
const {inject} = require(`./tooling/lib/openh264`);
const dotenv = require(`dotenv`);
const glob = require(`glob`);
const path = require(`path`);
const os = require(`os`);
const webpackConfig = require(`./webpack.config`);

dotenv.config({path: `.env.default`});
dotenv.config();

require(`babel-register`)({
  only: [
    `./packages/node_modules/**/*.js`
  ],
  sourceMaps: true
});

const PORT = process.env.PORT || 8000;
const CI = !!(process.env.JENKINS || process.env.CI);

exports.config = {
  //
  // ==================
  // Specify Test Files
  // ==================
  // Define which test specs should run. The pattern is relative to the directory
  // from which `wdio` was called. Notice that, if you are calling `wdio` from an
  // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
  // directory is where your package.json resides, so `wdio` will be called from there.
  //
  specs: [
    `./packages/node_modules/{*,*/*}/test/wdio/spec/**/*.js`
  ],
  suites: glob
    .sync(`**/package.json`, {cwd: `./packages/node_modules`})
    .map((p) => path.dirname(p))
    .reduce((suites, p) => {
      suites[p] = [
        `./packages/node_modules/${p}/test/wdio/spec/**/*.js`
      ];
      return suites;
    }, {}),
  //
  // ============
  // Capabilities
  // ============
  // Define your capabilities here. WebdriverIO can run multiple capabilities at the same
  // time. Depending on the number of capabilities, WebdriverIO launches several test
  // sessions. Within your capabilities you can overwrite the spec and exclude options in
  // order to group specific specs to a specific capability.
  //
  // First, you can define how many instances should be started at the same time. Let's
  // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
  // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
  // files and you set maxInstances to 10, all spec files will get tested at the same time
  // and 30 processes will get spawned. The property handles how many capabilities
  // from the same test should run tests.
  //
  maxInstances: 10,
  //
  // If you have trouble getting all important capabilities together, check out the
  // Sauce Labs platform configurator - a great tool to configure your capabilities:
  // https://docs.saucelabs.com/reference/platforms-configurator
  //
  capabilities: {
    browserSpock: {
      desiredCapabilities: {
        browserName: `firefox`,
        tunnelIdentifier: process.env.SC_TUNNEL_IDENTIFIER
      }
    },
    browserMccoy: {
      desiredCapabilities: {
        browserName: `chrome`,
        chromeOptions: {
          args: [
            `--use-fake-device-for-media-stream`,
            `--use-fake-ui-for-media-stream`
          ]
        },
        tunnelIdentifier: process.env.SC_TUNNEL_IDENTIFIER
      }
    }
  },
  //
  // ===================
  // Test Configurations
  // ===================
  // Define all options that are relevant for the WebdriverIO instance here
  //
  // By default WebdriverIO commands are executed in a synchronous way using
  // the wdio-sync package. If you still want to run your tests in an async way
  // e.g. using promises you can set the sync option to false.
  sync: true,
  //
  // Level of logging verbosity: silent | verbose | command | data | result | error
  logLevel: `error`,
  //
  // Enables colors for log output.
  coloredLogs: true,
  //
  // If you only want to run your tests until a specific amount of tests have failed use
  // bail (default is 0 - don't bail, run all tests).
  bail: 0,
  //
  // Saves a screenshot to a given path if a command fails.
  screenshotPath: `./reports/screenshots/`,
  //
  // Set a base URL in order to shorten url command calls. If your url parameter starts
  // with "/", then the base url gets prepended.
  baseUrl: `http://localhost:${PORT}/packages/node_modules`,
  //
  // Default timeout for all waitFor* commands.
  waitforTimeout: 10000,
  //
  // Default timeout in milliseconds for request
  // if Selenium Grid doesn't send response
  connectionRetryTimeout: 90000,
  //
  // Default request retries count
  connectionRetryCount: 3,
  //
  // Initialize the browser instance with a WebdriverIO plugin. The object should have the
  // plugin name as key and the desired plugin options as properties. Make sure you have
  // the plugin installed before running any tests. The following plugins are currently
  // available:
  // WebdriverCSS: https://github.com/webdriverio/webdrivercss
  // WebdriverRTC: https://github.com/webdriverio/webdriverrtc
  // Browserevent: https://github.com/webdriverio/browserevent
  // plugins: {
  //     webdrivercss: {
  //         screenshotRoot: 'my-shots',
  //         failedComparisonsRoot: 'diffs',
  //         misMatchTolerance: 0.05,
  //         screenWidth: [320,480,640,1024]
  //     },
  //     webdriverrtc: {},
  //     browserevent: {}
  // },
  //
  // Test runner services
  // Services take over a specific job you don't want to take care of. They enhance
  // your test setup with almost no effort. Unlike plugins, they don't add new
  // commands. Instead, they hook themselves up into the test process.
  services: CI ? [
    `sauce`,
    `static-server`,
    `webpack`
  ] : [
    `selenium-standalone`,
    `static-server`,
    `webpack`
  ],
  staticServerFolders: [
    {mount: `/`, path: `.`}
  ],
  staticServerPort: PORT,
  webpackConfig,

  //
  // Framework you want to run your specs with.
  // The following are supported: Mocha, Jasmine, and Cucumber
  // see also: http://webdriver.io/guide/testrunner/frameworks.html
  //
  // Make sure you have the wdio adapter package for the specific framework installed
  // before running any tests.
  framework: `mocha`,
  //
  // Test reporter for stdout.
  // The only one supported by default is 'dot'
  // see also: http://webdriver.io/guide/testrunner/reporters.html
  reporters: CI ? [`spec`, `junit`] : [`spec`],
  reporterOptions: {
    junit: {
      outputDir: `./reports/junit/wdio`
    }
  },
  //
  // Options to be passed to Mocha.
  // See the full list at http://mochajs.org/
  mochaOpts: {
    require: [`mocha-steps`],
    timeout: 40000,
    ui: `bdd`
  },
  //
  // =====
  // Hooks
  // =====
  // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
  // it and to build services around it. You can either apply a single function or an array of
  // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
  // resolved to continue.
  /**
   * Gets executed once before all workers get launched.
   * @param {Object} config wdio configuration object
   * @param {Array.<Object>} capabilities list of capabilities details
   * @returns {Promise}
   */
  onPrepare(config, capabilities) {
    const defs = [
      capabilities.browserSpock.desiredCapabilities,
      capabilities.browserMccoy.desiredCapabilities
    ];

    const build = process.env.BUILD_NUMBER || `local-${process.env.USER}-wdio-${Date.now()}`;
    defs.forEach((d) => {
      if (CI) {
        d.build = build;
        // Set the base to SauceLabs so that inject() does its thing.
        d.base = `SauceLabs`;

        d.version = d.version || `latest`;
        d.platform = d.platform || `OS X 10.12`;
      }
      else {
        // Copy the base over so that inject() does its thing.
        d.base = d.browserName;
        d.platform = os.platform();
      }
    });

    // The openh264 profile seems to break tests locally; run the tests twice
    // and the plugin should download automatically.
    return CI ? inject(defs) : Promise.resolve()
      .then(() => {
        // Remove the base because it's not actually a selenium property
        defs.forEach((d) => Reflect.deleteProperty(d, `base`));
      });
  },
  /**
   * Gets executed just before initialising the webdriver session and test framework. It allows you
   * to manipulate configurations depending on the capability or spec.
   * @param {Object} config wdio configuration object
   * @param {Array.<Object>} capabilities list of capabilities details
   * @param {Array.<String>} specs List of spec file paths that are to be run
   */
  // beforeSession: function (config, capabilities, specs) {
  // },
  /**
   * Gets executed before test execution begins. At this point you can access to all global
   * variables like `browser`. It is the perfect place to define custom commands.
   * @param {Array.<Object>} capabilities list of capabilities details
   * @param {Array.<String>} specs List of spec file paths that are to be run
   */
  // before(capabilities, specs) {
  // }
  //
  /**
   * Hook that gets executed before the suite starts
   * @param {Object} suite suite details
   */
  // beforeSuite: function (suite) {
  // },
  /**
   * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
   * beforeEach in Mocha)
   */
  // beforeHook: function () {
  // },
  /**
   * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
   * afterEach in Mocha)
   */
  // afterHook: function () {
  // },
  /**
   * Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
   * @param {Object} test test details
   */
  // beforeTest: function (test) {
  // },
  /**
   * Runs before a WebdriverIO command gets executed.
   * @param {String} commandName hook command name
   * @param {Array} args arguments that command would receive
   */
  // beforeCommand: function (commandName, args) {
  // },
  /**
   * Runs after a WebdriverIO command gets executed
   * @param {String} commandName hook command name
   * @param {Array} args arguments that command would receive
   * @param {Number} result 0 - command success, 1 - command error
   * @param {Object} error error object if any
   */
  // afterCommand: function (commandName, args, result, error) {
  // },
  /**
   * Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
   * @param {Object} test test details
   * @returns {undefined}
   */
  afterTest(test) {
    if (!test.passed) {
      const logTypes = browser.logTypes();

      Object.keys(logTypes).forEach((browserId) => {
        console.log(logTypes[browserId].value);
        if (logTypes[browserId].value.includes(`browser`)) {
          const logs = browser.select(browserId).log(`browser`);
          if (logs.value.length) {
            console.error(`Test ${test.fullTitle} failed with the following log output from browser ${browserId}`);
            console.log(logs);
            console.error(logs
              .value
              .map((v) => `> ${v.message}`)
              .join(`\n`));
          }
          else {
            console.error(`Test ${test.fullTitle} failed but no logs were produced by browser ${browserId}`);
          }
        }
        else {
          console.error(`${test.fullTitle} failed but browser ${browserId} doesn't support log collection`);
        }
      });
    }
  }
  /**
   * Hook that gets executed after the suite has ended
   * @param {Object} suite suite details
   */
  // afterSuite: function (suite) {
  // },
  /**
   * Gets executed after all tests are done. You still have access to all global variables from
   * the test.
   * @param {Number} result 0 - test pass, 1 - test fail
   * @param {Array.<Object>} capabilities list of capabilities details
   * @param {Array.<String>} specs List of spec file paths that ran
   */
  // after: function (result, capabilities, specs) {
  // },
  /**
   * Gets executed right after terminating the webdriver session.
   * @param {Object} config wdio configuration object
   * @param {Array.<Object>} capabilities list of capabilities details
   * @param {Array.<String>} specs List of spec file paths that ran
   */
  // afterSession: function (config, capabilities, specs) {
  // },
  /**
   * Gets executed after all workers got shut down and the process is about to exit. It is not
   * possible to defer the end of the process using a promise.
   * @param {Object} exitCode 0 - success, 1 - fail
   */
  // onComplete: function(exitCode) {
  // }
};

if (CI) {
  // I couldn't get multiremote + sauce to work while letting wdio handle the
  // tunnel setup. use `npm run sauce:start` and `npm run sauce:run` to start
  // the tunnel and run tests
  exports.config = Object.assign(exports.config, {
    user: process.env.SAUCE_USERNAME,
    key: process.env.SAUCE_ACCESS_KEY,
    sauceConnect: false
  });
}
<|endoftext|>


<commit_before>/*
    ioBroker.sma-em Widget-Set

    version: "0.5.0"

    Copyright 10.2015-2016 Marcolotti<info@ct-j.de>

*/
"use strict";

// add translations for edit mode
if (vis.editMode) {
    $.extend(true, systemDictionary, {
        "myColor":          {"en": "myColor",       "de": "mainColor",  "ru": "Мой цвет"},
        "myColor_tooltip":  {
            "en": "Description of\x0AmyColor",
            "de": "Beschreibung von\x0AmyColor",
            "ru": "Описание\x0AmyColor"
        },
        "htmlText":         {"en": "htmlText",      "de": "htmlText",   "ru": "htmlText"},
        "group_extraMyset": {"en": "extraMyset",    "de": "extraMyset", "ru": "extraMyset"},
        "extraAttr":        {"en": "extraAttr",     "de": "extraAttr",  "ru": "extraAttr"}
    });
}

// add translations for non-edit mode
$.extend(true, systemDictionary, {
    "Instance":  {"en": "Instance", "de": "Instanz", "ru": "Инстанция"}
});

// this code can be placed directly in sma-em.html
vis.binds.sma-em = {
    version: "0.5.0",
    showVersion: function () {
        if (vis.binds.sma-em.version) {
            console.log('Version sma-em: ' + vis.binds.sma-em.version);
            vis.binds.sma-em.version = null;
        }
    },
	createWidget: function (widgetID, view, data, style) {
        var $div = $('#' + widgetID);
        // if nothing found => wait
        if (!$div.length) {
            return setTimeout(function () {
                vis.binds.sma-em.createWidget(widgetID, view, data, style);
            }, 100);
        }

        var text = '';
        text += 'OID: ' + data.oid + '

';
        text += 'OID value: ' + vis.states[data.oid + '.val'] + '
';
        text += 'Color: ' + data.myColor + '
';
        text += 'extraAttr: ' + data.extraAttr + '
';
        text += 'Browser instance: ' + vis.instance + '
';
        text += 'htmlText: <textarea readonly style="width:100%">' + (data.htmlText || '') + '</textarea>
';

        $('#' + widgetID).html(text);

        // subscribe on updates of value
        if (data.oid) {
            vis.states.bind(data.oid + '.val', function (e, newVal, oldVal) {
                $div.find('.sma-em-value').html(newVal);
            });
        }
    }
};

vis.binds.sma-em.showVersion();
<commit_msg>Delete sma-em.js<commit_after><|endoftext|>


<commit_before>var VINYLS = null;
var latestVinyl = null;
var vinylcount = 0;

var Main = (function()
{

  function _init(){
    Select.init(); // style selectboxes
    $("#colorpicker").spectrum({
      color: "#000000",
      showInput: true,
      preferedFormat: "hex6"
    });
  }

  // this is where stuff happens
	function _doAfterLogin(name, fbid){
		_addUserToDb(name, fbid);
    _updateForms(fbid);
		_getExistingData(fbid);
	}

	// Add username with his fb id to db
	function _addUserToDb(name, fbid){
  	//console.log('calling ajax with '+name+' and '+fbid);
  	$.ajax({
    		type: 'POST',
    		url: './php/createuser.php',
    		data: {
      		username: name,
      		facebookid: fbid
   		},
    		success: function (response) {
      		console.log("record has been saved in database: "+response);
    		},
    		error: function () {
      		console.warn('there is some error in the createuser ajax request');
    		}
  	});
	}

  // Update Add and Edit forms with hidden input containing FB ID
  function _updateForms(fbid){
    if($('#addvinylform').length){
      $('#addvinylform').prepend('<input type="hidden" id="hiddenfbidinput" name="fbid" value="'+fbid+'">');
    }
    else{
      $('#editvinylform').prepend('<input type="hidden" id="hiddenfbidinput" name="fbid" value="'+fbid+'">');
    }
  }

  // After logged in, get users vinyl Data and display it on success
	function _getExistingData(fbid){
    //console.log("calling _getExistingData");
		$.ajax({
    		type: 'POST',
    		url: './php/getuservinyls.php',
    		data: {
      		facebookid: fbid
   		},
    		success: function (response) {
          //console.log(response);
          if(response.length){
            VINYLS = $.parseJSON(response);
            _displayVinylData(VINYLS); // display the resceived data
          }
          else{ // no vinyls in DB yet
            console.log("no vinyls yet");
            $('.footable').hide();
            vinylcount = 0;
            $('#vinylcount').text(vinylcount);
          }
    		},
    		error: function () {
      		console.warn('there is some error in the getuservinyls ajax request');
    		}
  	});
	}

  // Build actual Vinyl List and display it
  function _displayVinylData(vinyls){
    console.log("call _displayVinylData");

    if(!$('.footable').is(':visible')){
      $('.footable').show();
    }

    var index = 0;

    $.each(vinyls, function(){
      content = '
';
      content += '

	'
      content += ''+vinyls[index].VinylID+'	';
      content += ''+vinyls[index].Artist+'	';
      content += ''+vinyls[index].Album+'	';
      content += ''+vinyls[index].Label+'	';
      content += ''+vinyls[index].Format+' '+vinyls[index].Type+'	';
      content += ''+vinyls[index].Count+'	'
      content += '
'+vinyls[index].Color+'
	'
      content += ''+vinyls[index].Releasedate+'	';
      content += 'iTunes	';
      content += ''+vinyls[index].Price+'	';
      content += '<audio controls onplay="Main.audioHandler()"><source src="'+vinyls[index].Sample+'" type="audio/mp4">Sorry. Your browser does not seem to support the m4a audio format.</audio>	';
      content += '	';
      // Video
      if(vinyls[index].Video != '-'){
        content += ''+vinyls[index].Video.replace(/(?:http:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=)?(.+)/g, '')+'	';
      }
      else{
        content += '-	';
      }
      // Tracklist
      var tracklist = vinyls[index].Tracklist.split(";");
      content += ''+tracklist[0]+'
'
      for(var i=1; i<tracklist.length; i++){
        content += tracklist[i]+'
'
      }
      content += '	';

      content += ''+vinyls[index].Genre+'	';
      content += '	';
      content += '
';

      $('#tablecontent').append(content);
      index += 1;
    });

    // redraw the whole table
    $('.footable').trigger('footable_initialize');

    // update vinyl count
    vinylcount = vinyls.length;
    $('#vinylcount').text(vinylcount);
  }

  // pause all other audio players when another audio is playing
  function _audioHandler(){
    console.log("playing!");
    // TODO: stop all others audio players..
  }

  // fetch vinyl Data before submitting form
  function _fetchData(){
    console.log("calling _fetchData");

    $('#searchbutton').hide();
    $('#searching').show();

    var pickercolor = $("#colorpicker").spectrum("get").toHexString();
    $('input[name=color]').val(pickercolor);
    console.log(pickercolor);

    var artist = $('input[name=artist]').val();
    var album = $('input[name=title]').val();

    var vinyl = {};
    var releaseID;

    setTimeout(function)
    $.when(
      // 1st get Release ID from Discogs
      $.getJSON('http://api.discogs.com/database/search?type=release&q=title:'+album+'%20AND%20artist:'+artist+'%20AND%20format:%22vinyl%22&callback=?',
        function(data){
          if(typeof data.data.results[0] === 'undefined'){  // nothing was found
            alert("Sorry, couldn't find this vinyl. Please try again.");
            return;
          }
          else{
            releaseID = data.data.results[0].id;
          }
        })
    ).done(function(){
      $.when(
        // 2nd get Discogs Release infos
        $.getJSON('http://api.discogs.com/releases/'+releaseID,
          function(data){
            console.log(data);

            vinyl.label = data.labels[0].name;
            vinyl.genre = data.genres.join(', ');
            vinyl.date = data.released;
            vinyl.artist = data.artists[0].name;
            vinyl.title = data.title;
            // check if tracklist is found
            if(typeof data.videos != 'undefined'){
              vinyl.tracklist = data.tracklist[0].position+". ";
              vinyl.tracklist += data.tracklist[0].title+" ";
              vinyl.tracklist += data.tracklist[0].duration+";"

              for(var i=1; i<data.tracklist.length; i++){
                vinyl.tracklist += data.tracklist[i].position+". ";
                vinyl.tracklist += data.tracklist[i].title+" - ";
                vinyl.tracklist += data.tracklist[i].duration+";"
              }
            }
            else{
              vinyl.tracklist = '-';
            }
            // check if videos are found
            if(typeof data.videos != 'undefined'){
              vinyl.video = data.videos[0].uri;
            }
            else{ // found videos
              vinyl.video = '-';
            }

            console.log(data.labels[0].name);
          })
      ).done(function(){
        $.when(
          // 3rd get Album ID from Deezer
          $.getJSON('http://api.deezer.com/search/album?q='+artist+' '+album+'&output=jsonp&callback=?',
            function(result1){
              //console.log(result1);
              if(typeof result1.data[0] === 'undefined'){  // nothing was found
                albumID = 'undefined';
              }
              else{
                albumID = result1.data[0].id;
              }
            })
        ).done(function(){
          $.when(
            // 4th get Duration, Deezer link and Artistpic from Deezer
            $.getJSON('http://api.deezer.com/album/'+albumID+'&output=jsonp&callback=?',
              function(data){
                if(albumID !== 'undefined'){
                  vinyl.duration = data.duration;
                  vinyl.deezerlink = data.link;
                  vinyl.artistPic = data.artist.picture;
                }
                else{
                  vinyl.duration = '-';
                  vinyl.deezerlink = '-';
                  vinyl.artistPic = '-';
                }
              })
          ).done(function(){
            $.when(
              // 5th get artwork, audio sample from iTunes
              $.getJSON('http://itunes.apple.com/search?term='+artist+' '+album+'&limit=1&callback=?',
                function(data) {
                  console.log("iTunes data:");
                  console.log(data);
                  if(data.results.length != 0){
                    vinyl.artworkUrl = data.results[0].artworkUrl100;
                    vinyl.sampleUrl = data.results[0].previewUrl;
                    vinyl.price = data.results[0].collectionPrice;
                    vinyl.itunesUrl = data.results[0].collectionViewUrl;
                  }
                  else{ // no data found on iTunes
                    vinyl.artworkUrl = "/img/vinyl_PH.svg";
                    vinyl.sampleUrl = "no sample available";
                    vinyl.price = "not found";
                    vinyl.itunesUrl = "not found";
                  }
                })
            ).done(function(){
              console.log(vinyl);

              // fill input values
              $('input[name=genre]').val(vinyl.genre);
              $('input[name=label]').val(vinyl.label);
              $('input[name=artistpic]').val(vinyl.artistPic);
              $('input[name=artwork]').val(vinyl.artworkUrl);
              $('input[name=sample]').val(vinyl.sampleUrl);
              $('input[name=itunes]').val(vinyl.itunesUrl);
              $('input[name=deezer]').val(vinyl.deezerlink);
              $('input[name=release]').val(vinyl.date);
              $('input[name=price]').val(vinyl.price);
              $('input[name=duration]').val(vinyl.duration);
              $('input[name=color]').val(pickercolor);
              $('input[name=video]').val(vinyl.video);
              $('input[name=artist-corrected]').val(vinyl.artist);
              $('input[name=album-corrected]').val(vinyl.title);
              $('input[name=tracklist]').val(vinyl.tracklist);

              // Preview the vinyl
              _showPreview(vinyl);

            });
          });
        });
      });
    });
  }

  // Show preview of vinyl after search
  function _showPreview(vinyl){
    // hide search button; show submit button; show preview
    $('#searchbutton, #searching').hide();
    $('#submitbutton').fadeIn();
    $('#preview').fadeIn();

    $('#preview .artwork').html('');
    $('#preview .label-release-genre').html(vinyl.label+', '+vinyl.genre+', '+vinyl.date);
    $('#preview .sample').html('<audio controls onplay="Main.audioHandler()"><source src="'+vinyl.sampleUrl+'" type="audio/mp4">Sorry. Your browser does not seem to support the m4a audio format.</audio>');
  }

  // reset the "Add Vinyl" Form
  function _resetOverlay(){
    $('#overlay').fadeOut(200, function(){
      // check which form is present and remove it
      if($('#addvinylform').length){
        $('#addvinylform').remove();
      }
      else{
        $('#editvinylform').remove();
      }
    });
  }

	return{
    init: _init,
		doAfterLogin: _doAfterLogin,
		addUserToDb: _addUserToDb,
    updateForms: _updateForms,
		getExistingData: _getExistingData,
    displayVinylData: _displayVinylData,
    audioHandler: _audioHandler,
    fetchData: _fetchData,
    showPreview: _showPreview,
    resetOverlay: _resetOverlay
	}

})();

<commit_msg>show search button again after failed search<commit_after>var VINYLS = null;
var latestVinyl = null;
var vinylcount = 0;

var Main = (function()
{

  function _init(){
    Select.init(); // style selectboxes
    $("#colorpicker").spectrum({
      color: "#000000",
      showInput: true,
      preferedFormat: "hex6"
    });
  }

  // this is where stuff happens
	function _doAfterLogin(name, fbid){
		_addUserToDb(name, fbid);
    _updateForms(fbid);
		_getExistingData(fbid);
	}

	// Add username with his fb id to db
	function _addUserToDb(name, fbid){
  	//console.log('calling ajax with '+name+' and '+fbid);
  	$.ajax({
    		type: 'POST',
    		url: './php/createuser.php',
    		data: {
      		username: name,
      		facebookid: fbid
   		},
    		success: function (response) {
      		console.log("record has been saved in database: "+response);
    		},
    		error: function () {
      		console.warn('there is some error in the createuser ajax request');
    		}
  	});
	}

  // Update Add and Edit forms with hidden input containing FB ID
  function _updateForms(fbid){
    if($('#addvinylform').length){
      $('#addvinylform').prepend('<input type="hidden" id="hiddenfbidinput" name="fbid" value="'+fbid+'">');
    }
    else{
      $('#editvinylform').prepend('<input type="hidden" id="hiddenfbidinput" name="fbid" value="'+fbid+'">');
    }
  }

  // After logged in, get users vinyl Data and display it on success
	function _getExistingData(fbid){
    //console.log("calling _getExistingData");
		$.ajax({
    		type: 'POST',
    		url: './php/getuservinyls.php',
    		data: {
      		facebookid: fbid
   		},
    		success: function (response) {
          //console.log(response);
          if(response.length){
            VINYLS = $.parseJSON(response);
            _displayVinylData(VINYLS); // display the resceived data
          }
          else{ // no vinyls in DB yet
            console.log("no vinyls yet");
            $('.footable').hide();
            vinylcount = 0;
            $('#vinylcount').text(vinylcount);
          }
    		},
    		error: function () {
      		console.warn('there is some error in the getuservinyls ajax request');
    		}
  	});
	}

  // Build actual Vinyl List and display it
  function _displayVinylData(vinyls){
    console.log("call _displayVinylData");

    if(!$('.footable').is(':visible')){
      $('.footable').show();
    }

    var index = 0;

    $.each(vinyls, function(){
      content = '
';
      content += '

	'
      content += ''+vinyls[index].VinylID+'	';
      content += ''+vinyls[index].Artist+'	';
      content += ''+vinyls[index].Album+'	';
      content += ''+vinyls[index].Label+'	';
      content += ''+vinyls[index].Format+' '+vinyls[index].Type+'	';
      content += ''+vinyls[index].Count+'	'
      content += '
'+vinyls[index].Color+'
	'
      content += ''+vinyls[index].Releasedate+'	';
      content += 'iTunes	';
      content += ''+vinyls[index].Price+'	';
      content += '<audio controls onplay="Main.audioHandler()"><source src="'+vinyls[index].Sample+'" type="audio/mp4">Sorry. Your browser does not seem to support the m4a audio format.</audio>	';
      content += '	';
      // Video
      if(vinyls[index].Video != '-'){
        content += ''+vinyls[index].Video.replace(/(?:http:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=)?(.+)/g, '')+'	';
      }
      else{
        content += '-	';
      }
      // Tracklist
      var tracklist = vinyls[index].Tracklist.split(";");
      content += ''+tracklist[0]+'
'
      for(var i=1; i<tracklist.length; i++){
        content += tracklist[i]+'
'
      }
      content += '	';

      content += ''+vinyls[index].Genre+'	';
      content += '	';
      content += '
';

      $('#tablecontent').append(content);
      index += 1;
    });

    // redraw the whole table
    $('.footable').trigger('footable_initialize');

    // update vinyl count
    vinylcount = vinyls.length;
    $('#vinylcount').text(vinylcount);
  }

  // pause all other audio players when another audio is playing
  function _audioHandler(){
    console.log("playing!");
    // TODO: stop all others audio players..
  }

  // fetch vinyl Data before submitting form
  function _fetchData(){
    console.log("calling _fetchData");

    $('#searchbutton').hide();
    $('#searching').show();

    var pickercolor = $("#colorpicker").spectrum("get").toHexString();
    $('input[name=color]').val(pickercolor);
    console.log(pickercolor);

    var artist = $('input[name=artist]').val();
    var album = $('input[name=title]').val();

    var vinyl = {};
    var releaseID;

    $.when(
      // 1st get Release ID from Discogs
      $.getJSON('http://api.discogs.com/database/search?type=release&q=title:'+album+'%20AND%20artist:'+artist+'%20AND%20format:%22vinyl%22&callback=?',
        function(data){
          if(typeof data.data.results[0] === 'undefined'){  // nothing was found
            alert("Sorry, couldn't find this vinyl. Please try again.");
            $('#searchbutton').show();
            $('#searching').hide();
            return;
          }
          else{
            releaseID = data.data.results[0].id;
          }
        })
    ).done(function(){
      $.when(
        // 2nd get Discogs Release infos
        $.getJSON('http://api.discogs.com/releases/'+releaseID,
          function(data){
            console.log(data);

            vinyl.label = data.labels[0].name;
            vinyl.genre = data.genres.join(', ');
            vinyl.date = data.released;
            vinyl.artist = data.artists[0].name;
            vinyl.title = data.title;
            // check if tracklist is found
            if(typeof data.videos != 'undefined'){
              vinyl.tracklist = data.tracklist[0].position+". ";
              vinyl.tracklist += data.tracklist[0].title+" ";
              vinyl.tracklist += data.tracklist[0].duration+";"

              for(var i=1; i<data.tracklist.length; i++){
                vinyl.tracklist += data.tracklist[i].position+". ";
                vinyl.tracklist += data.tracklist[i].title+" - ";
                vinyl.tracklist += data.tracklist[i].duration+";"
              }
            }
            else{
              vinyl.tracklist = '-';
            }
            // check if videos are found
            if(typeof data.videos != 'undefined'){
              vinyl.video = data.videos[0].uri;
            }
            else{ // found videos
              vinyl.video = '-';
            }

            console.log(data.labels[0].name);
          })
      ).done(function(){
        $.when(
          // 3rd get Album ID from Deezer
          $.getJSON('http://api.deezer.com/search/album?q='+artist+' '+album+'&output=jsonp&callback=?',
            function(result1){
              //console.log(result1);
              if(typeof result1.data[0] === 'undefined'){  // nothing was found
                albumID = 'undefined';
              }
              else{
                albumID = result1.data[0].id;
              }
            })
        ).done(function(){
          $.when(
            // 4th get Duration, Deezer link and Artistpic from Deezer
            $.getJSON('http://api.deezer.com/album/'+albumID+'&output=jsonp&callback=?',
              function(data){
                if(albumID !== 'undefined'){
                  vinyl.duration = data.duration;
                  vinyl.deezerlink = data.link;
                  vinyl.artistPic = data.artist.picture;
                }
                else{
                  vinyl.duration = '-';
                  vinyl.deezerlink = '-';
                  vinyl.artistPic = '-';
                }
              })
          ).done(function(){
            $.when(
              // 5th get artwork, audio sample from iTunes
              $.getJSON('http://itunes.apple.com/search?term='+artist+' '+album+'&limit=1&callback=?',
                function(data) {
                  console.log("iTunes data:");
                  console.log(data);
                  if(data.results.length != 0){
                    vinyl.artworkUrl = data.results[0].artworkUrl100;
                    vinyl.sampleUrl = data.results[0].previewUrl;
                    vinyl.price = data.results[0].collectionPrice;
                    vinyl.itunesUrl = data.results[0].collectionViewUrl;
                  }
                  else{ // no data found on iTunes
                    vinyl.artworkUrl = "/img/vinyl_PH.svg";
                    vinyl.sampleUrl = "no sample available";
                    vinyl.price = "not found";
                    vinyl.itunesUrl = "not found";
                  }
                })
            ).done(function(){
              console.log(vinyl);

              // fill input values
              $('input[name=genre]').val(vinyl.genre);
              $('input[name=label]').val(vinyl.label);
              $('input[name=artistpic]').val(vinyl.artistPic);
              $('input[name=artwork]').val(vinyl.artworkUrl);
              $('input[name=sample]').val(vinyl.sampleUrl);
              $('input[name=itunes]').val(vinyl.itunesUrl);
              $('input[name=deezer]').val(vinyl.deezerlink);
              $('input[name=release]').val(vinyl.date);
              $('input[name=price]').val(vinyl.price);
              $('input[name=duration]').val(vinyl.duration);
              $('input[name=color]').val(pickercolor);
              $('input[name=video]').val(vinyl.video);
              $('input[name=artist-corrected]').val(vinyl.artist);
              $('input[name=album-corrected]').val(vinyl.title);
              $('input[name=tracklist]').val(vinyl.tracklist);

              // Preview the vinyl
              _showPreview(vinyl);

            });
          });
        });
      });
    });
  }

  // Show preview of vinyl after search
  function _showPreview(vinyl){
    // hide search button; show submit button; show preview
    $('#searchbutton, #searching').hide();
    $('#submitbutton').fadeIn();
    $('#preview').fadeIn();

    $('#preview .artwork').html('');
    $('#preview .label-release-genre').html(vinyl.label+', '+vinyl.genre+', '+vinyl.date);
    $('#preview .sample').html('<audio controls onplay="Main.audioHandler()"><source src="'+vinyl.sampleUrl+'" type="audio/mp4">Sorry. Your browser does not seem to support the m4a audio format.</audio>');
  }

  // reset the "Add Vinyl" Form
  function _resetOverlay(){
    $('#overlay').fadeOut(200, function(){
      // check which form is present and remove it
      if($('#addvinylform').length){
        $('#addvinylform').remove();
      }
      else{
        $('#editvinylform').remove();
      }
    });
  }

	return{
    init: _init,
		doAfterLogin: _doAfterLogin,
		addUserToDb: _addUserToDb,
    updateForms: _updateForms,
		getExistingData: _getExistingData,
    displayVinylData: _displayVinylData,
    audioHandler: _audioHandler,
    fetchData: _fetchData,
    showPreview: _showPreview,
    resetOverlay: _resetOverlay
	}

})();

<|endoftext|>