<commit_before>const request       = require('request-promise')
const config        = require('./data/client.json')
const Util          = require('./Util')

let DiscordServer;

module.exports =
// A representation of a Discord guild member.
class DiscordMember {
    constructor(discordServer, id) {
        this.discordServer = discordServer;
        this.id = id;

        this.bot = this.discordServer.bot;
        this.server = this.discordServer.server;

        // Have to do this to prevent circular reference in file requires
        // Gets a reference to DiscordServer so we can run static methods
        DiscordServer = this.discordServer.constructor;
    }

    static async new(discordServer, id) {
        let discordMember = new DiscordMember(discordServer, id);
        discordMember.prepareMember();
        return discordMember;
    }

    // Clears the member cache for a specific Discord user.
    clearCache() {
        if (DiscordServer.DataCache) {
            delete DiscordServer.DataCache[this.id];
        }
    }

    async prepareMember() {
        this.user = await this.bot.fetchUser(this.id, false);
        this.member = await this.server.fetchMember(this.user, false);
    }

    // Gets a member's nickname, formatted with this server's
    // specific settings.
    getNickname(data) {
        return Util.formatDataString(this.discordServer.getSetting('nicknameFormat'), data, this.member);
    }

    // This method is called to update the state of a specific member
    // in this Discord server.
    async verify(options) {
        var options = options || {};
        let data = {};

        if (!this.member) {
            await this.prepareMember();
        }

        try {
            // Read user data from memory, or request it if there isn't any cached.
            data = DiscordServer.DataCache[this.id] || await request({
                uri: `https://verify.eryn.io/api/user/${this.id}`,
                json: true,
                simple: false
            })
        } catch (e) {
            console.log(e);
            return {
                status: false,
                error: "Unknown error."
             }
        }

        // If the status is ok, the user is in the database.
        if (data.status === "ok"){
            // Cache the data for future use.
            DiscordServer.DataCache[this.id] = data;

            try {

                // Check if these settings are enabled for this specific server,
                // if so, then put the member in the correct state.

                if (this.discordServer.getSetting('nicknameUsers')) {
                    await this.member.setNickname(this.getNickname(data));
                }

                if (this.discordServer.getSetting('verifiedRole')) {
                    await this.member.addRole(this.discordServer.getSetting('verifiedRole'));
                }

                if (this.discordServer.getSetting('verifiedRemovedRole')){
                    await this.member.removeRole(this.discordServer.getSetting('verifiedRemovedRole'));
                }

                if (this.discordServer.getSetting('announceChannel') && options.announce !== false) {
                    let channel = await this.server.channels.get(this.discordServer.getSetting('announceChannel'));

                    if (channel) {
                        channel.send(`**User verified:** <@${this.id}> as ${data.robloxUsername}`);
                    }
                }

                // Check if we want to resolve group rank bindings with cached or fresh data.
                if (options.clearBindingsCache !== false) {
                    DiscordServer.BindingsCache[data.robloxId] = {};
                }

                // Resolve group rank bindings for this member.
                if (this.discordServer.getSetting('groupRankBindings').length > 0) {
                    for (let binding of this.discordServer.getSetting('groupRankBindings')) {
                        // We use a Promise.then here so that they all execute asynchronously.
                        DiscordServer.resolveGroupRankBinding(binding, data.robloxId)
                            .then((state) => {
                                if (state === true) {
                                    this.member.addRole(binding.role);
                                } else {
                                    this.member.removeRole(binding.role);
                                }
                            })
                            .catch((e) => {
                                console.log(e);
                                console.log('Resolution error for binding');
                            });
                    }
                }
            } catch (e) {
                console.log(e);
                // If anything failed here, it's most likely because the bot
                // couldn't modify the member due to a permission problem.
                return {
                    status: false,
                    nonFatal: true,
                    error: "RoVer couldn't modify the member in this server. Either the bot doesn't have permission or the target user cannot be modified by the bot (such as higher rank in the server)."
                }
            }

            return {
                status: true,
                robloxUsername: data.robloxUsername,
                robloxId: data.robloxId,
                discordId: this.member.id,
                discordName: this.member.user.username
            }
        } else {
            // Status was not "ok".
             switch (data.errorCode){
                case 404:
                    // User isn't in the database.
                    return {
                        status: false,
                        error: "Not verified. Go to https://verify.eryn.io to verify."
                    }
                case 429:
                    // This client has exceeded the amount of requests allowed in a 60 second period.
                    return {
                        status: false,
                        error: "Server is busy. Please try again later."
                    }
                default:
                    // Something else is wrong.
                    return {
                        status: false,
                        error: "Unknown error."
                    }
            }
        }
    }
}<commit_msg>Set maxSockets to Infinity<commit_after>const config        = require('./data/client.json')
const Util          = require('./Util')

const request       = require('request-promise').defaults({pool: {maxSockets: Infinity}});

let DiscordServer;

module.exports =
// A representation of a Discord guild member.
class DiscordMember {
    constructor(discordServer, id) {
        this.discordServer = discordServer;
        this.id = id;

        this.bot = this.discordServer.bot;
        this.server = this.discordServer.server;

        // Have to do this to prevent circular reference in file requires
        // Gets a reference to DiscordServer so we can run static methods
        DiscordServer = this.discordServer.constructor;
    }

    static async new(discordServer, id) {
        let discordMember = new DiscordMember(discordServer, id);
        discordMember.prepareMember();
        return discordMember;
    }

    // Clears the member cache for a specific Discord user.
    clearCache() {
        if (DiscordServer.DataCache) {
            delete DiscordServer.DataCache[this.id];
        }
    }

    async prepareMember() {
        this.user = await this.bot.fetchUser(this.id, false);
        this.member = await this.server.fetchMember(this.user, false);
    }

    // Gets a member's nickname, formatted with this server's
    // specific settings.
    getNickname(data) {
        return Util.formatDataString(this.discordServer.getSetting('nicknameFormat'), data, this.member);
    }

    // This method is called to update the state of a specific member
    // in this Discord server.
    async verify(options) {
        var options = options || {};
        let data = {};

        if (!this.member) {
            await this.prepareMember();
        }

        try {
            // Read user data from memory, or request it if there isn't any cached.
            data = DiscordServer.DataCache[this.id] || await request({
                uri: `https://verify.eryn.io/api/user/${this.id}`,
                json: true,
                simple: false
            });
        } catch (e) {
            console.log(e);
            return {
                status: false,
                error: "Unknown error."
             }
        }

        // If the status is ok, the user is in the database.
        if (data.status === "ok") {
            // Cache the data for future use.
            DiscordServer.DataCache[this.id] = data;

            try {

                // Check if these settings are enabled for this specific server,
                // if so, then put the member in the correct state.

                if (this.discordServer.getSetting('nicknameUsers')) {
                    await this.member.setNickname(this.getNickname(data));
                }

                if (this.discordServer.getSetting('verifiedRole')) {
                    await this.member.addRole(this.discordServer.getSetting('verifiedRole'));
                }

                if (this.discordServer.getSetting('verifiedRemovedRole')){
                    await this.member.removeRole(this.discordServer.getSetting('verifiedRemovedRole'));
                }

                if (this.discordServer.getSetting('announceChannel') && options.announce !== false) {
                    let channel = await this.server.channels.get(this.discordServer.getSetting('announceChannel'));

                    if (channel) {
                        channel.send(`**User verified:** <@${this.id}> as ${data.robloxUsername}`);
                    }
                }

                // Check if we want to resolve group rank bindings with cached or fresh data.
                if (options.clearBindingsCache !== false) {
                    DiscordServer.BindingsCache[data.robloxId] = {};
                }

                // Resolve group rank bindings for this member.
                if (this.discordServer.getSetting('groupRankBindings').length > 0) {
                    for (let binding of this.discordServer.getSetting('groupRankBindings')) {
                        // We use a Promise.then here so that they all execute asynchronously.
                        DiscordServer.resolveGroupRankBinding(binding, data.robloxId)
                            .then((state) => {
                                if (state === true) {
                                    this.member.addRole(binding.role);
                                } else {
                                    this.member.removeRole(binding.role);
                                }
                            })
                            .catch((e) => {
                                console.log(e);
                                console.log('Resolution error for binding');
                            });
                    }
                }
            } catch (e) {
                console.log(e);
                // If anything failed here, it's most likely because the bot
                // couldn't modify the member due to a permission problem.
                return {
                    status: false,
                    nonFatal: true,
                    error: "RoVer couldn't modify the member in this server. Either the bot doesn't have permission or the target user cannot be modified by the bot (such as higher rank in the server)."
                }
            }

            return {
                status: true,
                robloxUsername: data.robloxUsername,
                robloxId: data.robloxId,
                discordId: this.member.id,
                discordName: this.member.user.username
            }
        } else {
            // Status was not "ok".
             switch (data.errorCode){
                case 404:
                    // User isn't in the database.
                    return {
                        status: false,
                        error: "Not verified. Go to https://verify.eryn.io to verify."
                    }
                case 429:
                    // This client has exceeded the amount of requests allowed in a 60 second period.
                    return {
                        status: false,
                        error: "Server is busy. Please try again later."
                    }
                default:
                    // Something else is wrong.
                    return {
                        status: false,
                        error: "Unknown error."
                    }
            }
        }
    }
}<|endoftext|>


<commit_before>'use strict';
class RenderingContext {
  constructor( canvas ) {
    this.canvas  = canvas;
    this.gl      = canvas.getContext( 'webgl' ) ||
                      canvas.getContext( 'experimental-webgl' );
  }

  createProgram( ids ) {
    let gl      = this.gl;
    let program = gl.createProgram();

    ids.forEach( ( id ) => {
      gl.attachShader( program, this.createShader( id ) );
    } );

    gl.linkProgram( program );

    if ( !gl.getProgramParameter( program, gl.LINK_STATUS ) ) {
      console.error( gl.getProgramInfoLog( program ) );
    }

    return program;
  }

  useProgram( program ) {
    this.gl.useProgram( program );
  }

  createShader( id ) {
    let gl     = this.gl;
    let source = document.getElementById( id );
    let shader;

    switch ( source.type ) {
    case 'x-shader/x-vertex':
      shader = gl.createShader( gl.VERTEX_SHADER );
      break;
    case 'x-shader/x-fragment':
      shader = gl.createShader( gl.FRAGMENT_SHADER );
      break;
    default :
      console.error( 'The shader type is not an accepted value.' );
    }

    gl.shaderSource( shader, source.text );
    gl.compileShader( shader );

    if ( !gl.getShaderParameter( shader, gl.COMPILE_STATUS ) ) {
      console.error( gl.getShaderInfoLog( shader ) );
    }

    return shader;
  }

  bindVbos( vboAttribs ) {
    vboAttribs.forEach( ( vboAttrib ) => {
      this.bindVbo( vboAttrib.name, vboAttrib.vertices, vboAttrib.stride );
    });
  }

  bindVbo( name, vertices, stride ) {
    let gl       = this.gl;
    let program  = this.program;
    let location = gl.getAttribLocation( program, name );
    gl.bindBuffer( gl.ARRAY_BUFFER, this.createVbo( vertices ) );
    gl.enableVertexAttribArray( location );
    gl.vertexAttribPointer( location, stride, gl.FLOAT, false, 0, 0 );
  }

  createVbo( vertices ) {
    let gl  = this.gl;
    let vbo = gl.createBuffer();
    gl.bindBuffer( gl.ARRAY_BUFFER, vbo );
    gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( vertices ), gl.STATIC_DRAW );
    gl.bindBuffer( gl.ARRAY_BUFFER, null );
    return vbo;
  }

  bindIbo( indexes ) {
    let gl  = this.gl;
    let ibo = gl.createBuffer();
    gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, ibo );
    gl.bufferData( gl.ELEMENT_ARRAY_BUFFER, new Int16Array( indexes ), gl.STATIC_DRAW );
    gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, null );
    gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, ibo );
  }

  enable( cap ) {
    this.gl.enable( cap );
  }

  disable( cap ) {
    this.gl.disable( cap );
  }

  depthFunc( type ) {
    this.gl.depthFunc( type );
  }

  // uniform bind
  bindUniforms( uniformAttribs ) {
    uniformAttribs.forEach( ( uniformAttrib ) =>
                            this.bindUniform( uniformAttrib.name,
                                              uniformAttrib.type,
                                              uniformAttrib.value ) );
  }

  bindUniform( name, type, value ) {
    let gl = this.gl;
    let location = gl.getUniformLocation( this.program, name );

    switch ( type ) {
    case 'matrix4fv':
      gl.uniformMatrix4fv( location, false, value );
      break;
    case '4fv':
      gl.uniform4fv( location, value );
      break;
    case '3fv':
      gl.uniform3fv( location, value );
      break;
    case '2fv':
      gl.uniform2fv( location, value );
      break;
    case '1fv':
      gl.uniform1fv( location, value );
      break;
    case '1f':
      gl.uniform1f( location, value );
      break;
    case '1iv':
      gl.uniform1iv( location, value );
      break;
    case '1i':
      gl.uniform1i( location, value );
      break;
    default:
    }
  }

  clear( color, depth ) {
    let gl = this.gl;
    let flag = gl.COLOR_BUFFER_BIT;

    gl.clearColor( color.r, color.g, color.b, color.a );

    if ( depth ) {
      gl.clearDepth( depth );
      flag = flag | gl.DEPTH_BUFFER_BIT;
    }

    gl.clear( flag );
  }

  drawArrays( mode, count, first = 0 ) {
    let gl = this.gl;
    gl.drawArrays( mode, first, count );
  }

  drawElements( mode, count, offset = 0 ) {
    let gl = this.gl;
    gl.drawElements( mode, count, gl.UNSIGNED_SHORT, offset );
  }
}

export {RenderingContext};
<commit_msg>:heavy_plus_sign: Add a viewport method.<commit_after>'use strict';
class RenderingContext {
  constructor( canvas ) {
    this.canvas  = canvas;
    this.gl      = canvas.getContext( 'webgl' ) ||
                      canvas.getContext( 'experimental-webgl' );
  }

  createProgram( ids ) {
    let gl      = this.gl;
    let program = gl.createProgram();

    ids.forEach( ( id ) => {
      gl.attachShader( program, this.createShader( id ) );
    } );

    gl.linkProgram( program );

    if ( !gl.getProgramParameter( program, gl.LINK_STATUS ) ) {
      console.error( gl.getProgramInfoLog( program ) );
    }

    return program;
  }

  useProgram( program ) {
    this.gl.useProgram( program );
  }

  createShader( id ) {
    let gl     = this.gl;
    let source = document.getElementById( id );
    let shader;

    switch ( source.type ) {
    case 'x-shader/x-vertex':
      shader = gl.createShader( gl.VERTEX_SHADER );
      break;
    case 'x-shader/x-fragment':
      shader = gl.createShader( gl.FRAGMENT_SHADER );
      break;
    default :
      console.error( 'The shader type is not an accepted value.' );
    }

    gl.shaderSource( shader, source.text );
    gl.compileShader( shader );

    if ( !gl.getShaderParameter( shader, gl.COMPILE_STATUS ) ) {
      console.error( gl.getShaderInfoLog( shader ) );
    }

    return shader;
  }

  bindVbos( vboAttribs ) {
    vboAttribs.forEach( ( vboAttrib ) => {
      this.bindVbo( vboAttrib.name, vboAttrib.vertices, vboAttrib.stride );
    });
  }

  bindVbo( name, vertices, stride ) {
    let gl       = this.gl;
    let program  = this.program;
    let location = gl.getAttribLocation( program, name );
    gl.bindBuffer( gl.ARRAY_BUFFER, this.createVbo( vertices ) );
    gl.enableVertexAttribArray( location );
    gl.vertexAttribPointer( location, stride, gl.FLOAT, false, 0, 0 );
  }

  createVbo( vertices ) {
    let gl  = this.gl;
    let vbo = gl.createBuffer();
    gl.bindBuffer( gl.ARRAY_BUFFER, vbo );
    gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( vertices ), gl.STATIC_DRAW );
    gl.bindBuffer( gl.ARRAY_BUFFER, null );
    return vbo;
  }

  bindIbo( indexes ) {
    let gl  = this.gl;
    let ibo = gl.createBuffer();
    gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, ibo );
    gl.bufferData( gl.ELEMENT_ARRAY_BUFFER, new Int16Array( indexes ), gl.STATIC_DRAW );
    gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, null );
    gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, ibo );
  }

  enable( cap ) {
    this.gl.enable( cap );
  }

  disable( cap ) {
    this.gl.disable( cap );
  }

  depthFunc( type ) {
    this.gl.depthFunc( type );
  }

  // uniform bind
  bindUniforms( uniformAttribs ) {
    uniformAttribs.forEach( ( uniformAttrib ) =>
                            this.bindUniform( uniformAttrib.name,
                                              uniformAttrib.type,
                                              uniformAttrib.value ) );
  }

  bindUniform( name, type, value ) {
    let gl = this.gl;
    let location = gl.getUniformLocation( this.program, name );

    switch ( type ) {
    case 'matrix4fv':
      gl.uniformMatrix4fv( location, false, value );
      break;
    case '4fv':
      gl.uniform4fv( location, value );
      break;
    case '3fv':
      gl.uniform3fv( location, value );
      break;
    case '2fv':
      gl.uniform2fv( location, value );
      break;
    case '1fv':
      gl.uniform1fv( location, value );
      break;
    case '1f':
      gl.uniform1f( location, value );
      break;
    case '1iv':
      gl.uniform1iv( location, value );
      break;
    case '1i':
      gl.uniform1i( location, value );
      break;
    default:
    }
  }

  clear( color, depth ) {
    let gl = this.gl;
    let flag = gl.COLOR_BUFFER_BIT;

    gl.clearColor( color.r, color.g, color.b, color.a );

    if ( depth ) {
      gl.clearDepth( depth );
      flag = flag | gl.DEPTH_BUFFER_BIT;
    }

    gl.clear( flag );
  }

  viewport( viewport ) {
    this.gl.viewport(
      viewport.x,
      viewport.y,
      viewport.width,
      viewport.height);
  }

  drawArrays( mode, count, first = 0 ) {
    let gl = this.gl;
    gl.drawArrays( mode, first, count );
  }

  drawElements( mode, count, offset = 0 ) {
    let gl = this.gl;
    gl.drawElements( mode, count, gl.UNSIGNED_SHORT, offset );
  }
}

export {RenderingContext};
<|endoftext|>


<commit_before>const moment = require('moment-timezone');
const path = require('path');
const fs = require('fs-extra');
const membersCSV = require('@tryghost/members-csv');
const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');

const emailTemplate = require('./email-template');

const messages = {
    filenameCollision: 'Filename already exists, please try again.',
    jobAlreadyComplete: 'Job is already complete.'
};

module.exports = class MembersCSVImporter {
    /**
     * @param {Object} config
     * @param {string} config.storagePath - The path to store CSV's in before importing
     * @param {Object} settingsCache - An instance of the Ghost Settings Cache
     * @param {() => Object} getMembersApi
     * @param {Object} ghostMailer - An instance of GhostMailer
     * @param {(string) => boolean} isSet - Method checking if specific feature is enabled
     * @param {({name, at, job, data, offloaded}) => void} addJob - Method registering an async job
     * @param {Object} knex - An instance of the Ghost Database connection
     * @param {Function} urlFor - function generating urls
     */
    constructor(config, settingsCache, getMembersApi, ghostMailer, isSet, addJob, knex, urlFor) {
        this._storagePath = config.storagePath;
        this._settingsCache = settingsCache;
        this._getMembersApi = getMembersApi;
        this._ghostMailer = ghostMailer;
        this._isSet = isSet;
        this._addJob = addJob;
        this._knex = knex;
        this._urlFor = urlFor;
    }

    /**
     * @typedef {string} JobID
     */

    /**
     * @typedef {Object} Job
     * @prop {string} filename
     * @prop {JobID} id
     * @prop {string} status
     */

    /**
     * Get the Job for a jobCode
     * @param {JobID} jobId
     * @returns {Promise<Job>}
     */
    async getJob(jobId) {
        return {
            id: jobId,
            filename: jobId,
            status: 'pending'
        };
    }

    /**
     * Prepares a CSV file for import
     * - Maps headers based on headerMapping, this allows for a non standard CSV
     *   to be imported, so long as a mapping exists between it and a standard CSV
     * - Stores the CSV to be imported in the storagePath
     * - Creates a MemberImport Job and associated MemberImportBatch's
     *
     * @param {string} inputFilePath - The path to the CSV to prepare
     * @param {Object.<string, string>} headerMapping - An object whos keys are headers in the input CSV and values are the header to replace it with
     * @param {Array<string>} defaultLabels - A list of labels to apply to every member
     *
     * @returns {Promise<{id: JobID, batches: number, metadata: Object.<string, any>}>} - A promise resolving to the id of the MemberImport Job
     */
    async prepare(inputFilePath, headerMapping, defaultLabels) {
        const batchSize = 1;

        const siteTimezone = this._settingsCache.get('timezone');
        const currentTime = moment().tz(siteTimezone).format('YYYY-MM-DD HH:mm:ss.SSS');
        const outputFileName = `Members Import ${currentTime}.csv`;
        const outputFilePath = path.join(this._storagePath, '/', outputFileName);

        const pathExists = await fs.pathExists(outputFilePath);

        if (pathExists) {
            throw new errors.DataImportError(tpl(messages.filenameCollision));
        }

        const rows = await membersCSV.parse(inputFilePath, headerMapping, defaultLabels);
        const numberOfBatches = Math.ceil(rows.length / batchSize);
        const mappedCSV = membersCSV.unparse(rows);

        const hasStripeData = rows.find(function rowHasStripeData(row) {
            return !!row.stripe_customer_id || !!row.complimentary_plan;
        });

        await fs.writeFile(outputFilePath, mappedCSV);

        return {
            id: outputFilePath,
            batches: numberOfBatches,
            metadata: {
                hasStripeData
            }
        };
    }

    /**
     * Performs an import of a CSV file
     *
     * @param {JobID} id - The id of the job to perform
     */
    async perform(id) {
        const job = await this.getJob(id);

        if (job.status === 'complete') {
            throw new errors.BadRequestError(tpl(messages.jobAlreadyComplete));
        }

        const rows = membersCSV.parse(job.filename);

        const membersApi = await this._getMembersApi();

        const defaultProductPage = await membersApi.productRepository.list({
            limit: 1
        });

        const defaultProduct = defaultProductPage.data[0];

        const result = await rows.reduce(async (resultPromise, row) => {
            const resultAccumulator = await resultPromise;

            const trx = await this._knex.transaction();
            const options = {
                transacting: trx
            };

            try {
                const existingMember = await membersApi.members.get({email: row.email}, {
                    ...options,
                    withRelated: ['labels']
                });
                let member;
                if (existingMember) {
                    const existingLabels = existingMember.related('labels') ? existingMember.related('labels').toJSON() : [];
                    member = await membersApi.members.update({
                        ...row,
                        labels: existingLabels.concat(row.labels)
                    }, {
                        ...options,
                        id: existingMember.id
                    });
                } else {
                    member = await membersApi.members.create(row, options);
                }

                if (row.stripe_customer_id) {
                    await membersApi.members.linkStripeCustomer({
                        customer_id: row.stripe_customer_id,
                        member_id: member.id
                    }, options);
                } else if (row.complimentary_plan) {
                    if (!this._isSet('multipleProducts')) {
                        await membersApi.members.setComplimentarySubscription(member, options);
                    } else if (!row.products) {
                        await membersApi.members.update({
                            products: [{id: defaultProduct.id}]
                        }, {
                            ...options,
                            id: member.id
                        });
                    }
                }

                if (this._isSet('multipleProducts')) {
                    if (row.products) {
                        await membersApi.members.update({
                            products: row.products
                        }, {
                            ...options,
                            id: member.id
                        });
                    }
                }

                await trx.commit();
                return {
                    ...resultAccumulator,
                    imported: resultAccumulator.imported + 1
                };
            } catch (error) {
                // The model layer can sometimes throw arrays of errors
                const errorList = [].concat(error);
                const errorMessage = errorList.map(({message}) => message).join(', ');
                await trx.rollback();
                return {
                    ...resultAccumulator,
                    errors: [...resultAccumulator.errors, {
                        ...row,
                        error: errorMessage
                    }]
                };
            }
        }, Promise.resolve({
            imported: 0,
            errors: []
        }));

        return {
            total: result.imported + result.errors.length,
            ...result
        };
    }

    generateCompletionEmail(result, data) {
        const siteUrl = new URL(this._urlFor('home', null, true));
        const membersUrl = new URL('members', this._urlFor('admin', null, true));
        if (data.importLabel) {
            membersUrl.searchParams.set('label', data.importLabel.slug);
        }
        return emailTemplate({result, siteUrl, membersUrl, ...data});
    }

    generateErrorCSV(result) {
        const errorsWithFormattedMessages = result.errors.map((row) => {
            const formattedError = row.error
                .replace(
                    'Value in [members.email] cannot be blank.',
                    'Missing email address'
                )
                .replace(
                    'Value in [members.note] exceeds maximum length of 2000 characters.',
                    '"Note" exceeds maximum length of 2000 characters'
                )
                .replace(
                    'Value in [members.subscribed] must be one of true, false, 0 or 1.',
                    'Value in "Subscribed to emails" must be "true" or "false"'
                )
                .replace(
                    'Validation (isEmail) failed for email',
                    'Invalid email address'
                )
                .replace(
                    /No such customer:[^,]*/,
                    'Could not find Stripe customer'
                );

            return {
                ...row,
                error: formattedError
            };
        });
        return membersCSV.unparse(errorsWithFormattedMessages);
    }

    /**
     * Processes CSV file and imports member&label records depending on the size of the imported set
     *
     * @param {Object} config
     * @param {String} config.pathToCSV - path where imported csv with members records is stored
     * @param {Object} config.headerMapping - mapping of CSV headers to member record fields
     * @param {Object} [config.globalLabels] - labels to be applied to whole imported members set
     * @param {Object} config.importLabel -
     * @param {String} config.importLabel.name - label name
     * @param {Object} config.user
     * @param {String} config.user.email - calling user email
     * @param {Object} config.LabelModel - instance of Ghosts Label model
     */
    async process({pathToCSV, headerMapping, globalLabels, importLabel, user, LabelModel}) {
        const job = await this.prepare(pathToCSV, headerMapping, globalLabels);

        if (job.batches <= 500 && !job.metadata.hasStripeData) {
            const result = await this.perform(job.id);
            const importLabelModel = result.imported ? await LabelModel.findOne(importLabel) : null;
            return {
                meta: {
                    stats: {
                        imported: result.imported,
                        invalid: result.errors
                    },
                    import_label: importLabelModel
                }
            };
        } else {
            const emailRecipient = user.email;
            this._addJob({
                job: async () => {
                    const result = await this.perform(job.id);
                    const importLabelModel = result.imported ? await LabelModel.findOne(importLabel) : null;
                    const emailContent = this.generateCompletionEmail(result, {
                        emailRecipient,
                        importLabel: importLabelModel ? importLabelModel.toJSON() : null
                    });
                    const errorCSV = this.generateErrorCSV(result);
                    const emailSubject = result.imported > 0 ? 'Your member import is complete' : 'Your member import was unsuccessful';

                    await this._ghostMailer.send({
                        to: emailRecipient,
                        subject: emailSubject,
                        html: emailContent,
                        forceTextContent: true,
                        attachments: [{
                            filename: `${importLabel.name} - Errors.csv`,
                            contents: errorCSV,
                            contentType: 'text/csv',
                            contentDisposition: 'attachment'
                        }]
                    });
                },
                offloaded: false
            });

            return {};
        }
    }
};
<commit_msg>Refactored constructure signature to be a n object<commit_after>const moment = require('moment-timezone');
const path = require('path');
const fs = require('fs-extra');
const membersCSV = require('@tryghost/members-csv');
const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');

const emailTemplate = require('./email-template');

const messages = {
    filenameCollision: 'Filename already exists, please try again.',
    jobAlreadyComplete: 'Job is already complete.'
};

module.exports = class MembersCSVImporter {
    /**
     * @param {Object} options
     * @param {Object} options.config
     * @param {string} options.config.storagePath - The path to store CSV's in before importing
     * @param {Object} options.settingsCache - An instance of the Ghost Settings Cache
     * @param {() => Object} options.getMembersApi
     * @param {Object} options.ghostMailer - An instance of GhostMailer
     * @param {(string) => boolean} options.isSet - Method checking if specific feature is enabled
     * @param {({name, at, job, data, offloaded}) => void} options.addJob - Method registering an async job
     * @param {Object} options.knex - An instance of the Ghost Database connection
     * @param {Function} options.urlFor - function generating urls
     */
    constructor({config, settingsCache, getMembersApi, ghostMailer, isSet, addJob, knex, urlFor}) {
        this._storagePath = config.storagePath;
        this._settingsCache = settingsCache;
        this._getMembersApi = getMembersApi;
        this._ghostMailer = ghostMailer;
        this._isSet = isSet;
        this._addJob = addJob;
        this._knex = knex;
        this._urlFor = urlFor;
    }

    /**
     * @typedef {string} JobID
     */

    /**
     * @typedef {Object} Job
     * @prop {string} filename
     * @prop {JobID} id
     * @prop {string} status
     */

    /**
     * Get the Job for a jobCode
     * @param {JobID} jobId
     * @returns {Promise<Job>}
     */
    async getJob(jobId) {
        return {
            id: jobId,
            filename: jobId,
            status: 'pending'
        };
    }

    /**
     * Prepares a CSV file for import
     * - Maps headers based on headerMapping, this allows for a non standard CSV
     *   to be imported, so long as a mapping exists between it and a standard CSV
     * - Stores the CSV to be imported in the storagePath
     * - Creates a MemberImport Job and associated MemberImportBatch's
     *
     * @param {string} inputFilePath - The path to the CSV to prepare
     * @param {Object.<string, string>} headerMapping - An object whos keys are headers in the input CSV and values are the header to replace it with
     * @param {Array<string>} defaultLabels - A list of labels to apply to every member
     *
     * @returns {Promise<{id: JobID, batches: number, metadata: Object.<string, any>}>} - A promise resolving to the id of the MemberImport Job
     */
    async prepare(inputFilePath, headerMapping, defaultLabels) {
        const batchSize = 1;

        const siteTimezone = this._settingsCache.get('timezone');
        const currentTime = moment().tz(siteTimezone).format('YYYY-MM-DD HH:mm:ss.SSS');
        const outputFileName = `Members Import ${currentTime}.csv`;
        const outputFilePath = path.join(this._storagePath, '/', outputFileName);

        const pathExists = await fs.pathExists(outputFilePath);

        if (pathExists) {
            throw new errors.DataImportError(tpl(messages.filenameCollision));
        }

        const rows = await membersCSV.parse(inputFilePath, headerMapping, defaultLabels);
        const numberOfBatches = Math.ceil(rows.length / batchSize);
        const mappedCSV = membersCSV.unparse(rows);

        const hasStripeData = rows.find(function rowHasStripeData(row) {
            return !!row.stripe_customer_id || !!row.complimentary_plan;
        });

        await fs.writeFile(outputFilePath, mappedCSV);

        return {
            id: outputFilePath,
            batches: numberOfBatches,
            metadata: {
                hasStripeData
            }
        };
    }

    /**
     * Performs an import of a CSV file
     *
     * @param {JobID} id - The id of the job to perform
     */
    async perform(id) {
        const job = await this.getJob(id);

        if (job.status === 'complete') {
            throw new errors.BadRequestError(tpl(messages.jobAlreadyComplete));
        }

        const rows = membersCSV.parse(job.filename);

        const membersApi = await this._getMembersApi();

        const defaultProductPage = await membersApi.productRepository.list({
            limit: 1
        });

        const defaultProduct = defaultProductPage.data[0];

        const result = await rows.reduce(async (resultPromise, row) => {
            const resultAccumulator = await resultPromise;

            const trx = await this._knex.transaction();
            const options = {
                transacting: trx
            };

            try {
                const existingMember = await membersApi.members.get({email: row.email}, {
                    ...options,
                    withRelated: ['labels']
                });
                let member;
                if (existingMember) {
                    const existingLabels = existingMember.related('labels') ? existingMember.related('labels').toJSON() : [];
                    member = await membersApi.members.update({
                        ...row,
                        labels: existingLabels.concat(row.labels)
                    }, {
                        ...options,
                        id: existingMember.id
                    });
                } else {
                    member = await membersApi.members.create(row, options);
                }

                if (row.stripe_customer_id) {
                    await membersApi.members.linkStripeCustomer({
                        customer_id: row.stripe_customer_id,
                        member_id: member.id
                    }, options);
                } else if (row.complimentary_plan) {
                    if (!this._isSet('multipleProducts')) {
                        await membersApi.members.setComplimentarySubscription(member, options);
                    } else if (!row.products) {
                        await membersApi.members.update({
                            products: [{id: defaultProduct.id}]
                        }, {
                            ...options,
                            id: member.id
                        });
                    }
                }

                if (this._isSet('multipleProducts')) {
                    if (row.products) {
                        await membersApi.members.update({
                            products: row.products
                        }, {
                            ...options,
                            id: member.id
                        });
                    }
                }

                await trx.commit();
                return {
                    ...resultAccumulator,
                    imported: resultAccumulator.imported + 1
                };
            } catch (error) {
                // The model layer can sometimes throw arrays of errors
                const errorList = [].concat(error);
                const errorMessage = errorList.map(({message}) => message).join(', ');
                await trx.rollback();
                return {
                    ...resultAccumulator,
                    errors: [...resultAccumulator.errors, {
                        ...row,
                        error: errorMessage
                    }]
                };
            }
        }, Promise.resolve({
            imported: 0,
            errors: []
        }));

        return {
            total: result.imported + result.errors.length,
            ...result
        };
    }

    generateCompletionEmail(result, data) {
        const siteUrl = new URL(this._urlFor('home', null, true));
        const membersUrl = new URL('members', this._urlFor('admin', null, true));
        if (data.importLabel) {
            membersUrl.searchParams.set('label', data.importLabel.slug);
        }
        return emailTemplate({result, siteUrl, membersUrl, ...data});
    }

    generateErrorCSV(result) {
        const errorsWithFormattedMessages = result.errors.map((row) => {
            const formattedError = row.error
                .replace(
                    'Value in [members.email] cannot be blank.',
                    'Missing email address'
                )
                .replace(
                    'Value in [members.note] exceeds maximum length of 2000 characters.',
                    '"Note" exceeds maximum length of 2000 characters'
                )
                .replace(
                    'Value in [members.subscribed] must be one of true, false, 0 or 1.',
                    'Value in "Subscribed to emails" must be "true" or "false"'
                )
                .replace(
                    'Validation (isEmail) failed for email',
                    'Invalid email address'
                )
                .replace(
                    /No such customer:[^,]*/,
                    'Could not find Stripe customer'
                );

            return {
                ...row,
                error: formattedError
            };
        });
        return membersCSV.unparse(errorsWithFormattedMessages);
    }

    /**
     * Processes CSV file and imports member&label records depending on the size of the imported set
     *
     * @param {Object} config
     * @param {String} config.pathToCSV - path where imported csv with members records is stored
     * @param {Object} config.headerMapping - mapping of CSV headers to member record fields
     * @param {Object} [config.globalLabels] - labels to be applied to whole imported members set
     * @param {Object} config.importLabel -
     * @param {String} config.importLabel.name - label name
     * @param {Object} config.user
     * @param {String} config.user.email - calling user email
     * @param {Object} config.LabelModel - instance of Ghosts Label model
     */
    async process({pathToCSV, headerMapping, globalLabels, importLabel, user, LabelModel}) {
        const job = await this.prepare(pathToCSV, headerMapping, globalLabels);

        if (job.batches <= 500 && !job.metadata.hasStripeData) {
            const result = await this.perform(job.id);
            const importLabelModel = result.imported ? await LabelModel.findOne(importLabel) : null;
            return {
                meta: {
                    stats: {
                        imported: result.imported,
                        invalid: result.errors
                    },
                    import_label: importLabelModel
                }
            };
        } else {
            const emailRecipient = user.email;
            this._addJob({
                job: async () => {
                    const result = await this.perform(job.id);
                    const importLabelModel = result.imported ? await LabelModel.findOne(importLabel) : null;
                    const emailContent = this.generateCompletionEmail(result, {
                        emailRecipient,
                        importLabel: importLabelModel ? importLabelModel.toJSON() : null
                    });
                    const errorCSV = this.generateErrorCSV(result);
                    const emailSubject = result.imported > 0 ? 'Your member import is complete' : 'Your member import was unsuccessful';

                    await this._ghostMailer.send({
                        to: emailRecipient,
                        subject: emailSubject,
                        html: emailContent,
                        forceTextContent: true,
                        attachments: [{
                            filename: `${importLabel.name} - Errors.csv`,
                            contents: errorCSV,
                            contentType: 'text/csv',
                            contentDisposition: 'attachment'
                        }]
                    });
                },
                offloaded: false
            });

            return {};
        }
    }
};
<|endoftext|>


<commit_before>'use strict';

angular.module('mean.system').provider('$meanState', ['$stateProvider', '$viewPathProvider', function($stateProvider, $viewPathProvider) {
	this.state = function(stateName, data) {
		if (data.templateUrl) {
			data.templateUrl = $viewPathProvider.path(data.templateUrl);
		}
		$stateProvider.state(stateName, data);

		return this;
	};

	this.$get = function() {
		return this;
	};
}]);
<commit_msg>rerun build<commit_after>'use strict';

angular.module('mean.system').provider('$meanState', ['$stateProvider', '$viewPathProvider', function($stateProvider, $viewPathProvider) {
	this.state = function(stateName, data) {
		if (data.templateUrl) {
			data.templateUrl = $viewPathProvider.path(data.templateUrl);
		}
		$stateProvider.state(stateName, data);
		return this;
	};

	this.$get = function() {
		return this;
	};
}]);
<|endoftext|>


<commit_before>import _ from 'underscore'
import React from 'react'
import { connect } from 'react-redux'
import actions from '../../actions/deviceGroup'
import BaseWidget from './Base'
import { current as user } from '../../models/User'
import Checkbox from 'rc-checkbox'

import 'rc-checkbox/assets/index.css'

const mapStateToProps = (state) => {
	var { loading: isFetching, items } = state.deviceGroup.fetchAll
	return { isFetching, items }
}

const mapDispatchToProps = (dispatch) => {
	return {
		onFetchAll () {
			return dispatch(actions.fetchAll())
		},
	}
}

class DevicePickerWidget extends BaseWidget {
	constructor () {
		super()
		this.state = {
			deviceGroups: null,
		}
	}
	componentWillMount () {
		if (this.props.shared.deviceGroups || this.props.items) {
			this.onDeviceGroups(this.props)
		} else {
			this.props.onFetchAll()
		}
	}
	componentWillReceiveProps (next) {
		var deviceGroups = (next.shared.deviceGroups || next.items)

		if (deviceGroups && deviceGroups !== this.state.deviceGroups) {
			this.onDeviceGroups(next)
		}
	}
	onDeviceGroups (props) {
		// TODO(mauvm): Convert deviceGroups to model (e.g. filter.getUDIDs())
		// and set shared prop 'deviceFilter'

		var deviceGroups = (props.shared.deviceGroups || props.items)
		var filter = this.getURLFilter(props.fromURL)

		deviceGroups.forEach((group) => {
			var includeGroup = filter.deviceGroups[group.id]

			if (includeGroup === undefined) {
				includeGroup = filter.includeByDefault
			}

			group.getDevices().forEach((device) => {
				var includeDevice

				if ( ! props.groupsOnly) {
					includeDevice = filter.devices[device.id]
				}

				var include = (includeDevice !== undefined ? includeDevice : includeGroup)
				device.setAttribute('include', include)
			})
		})

		var deviceCount = _.reduce(
			deviceGroups.map((group) => group.getDevices().length),
			(memo, num) => memo + num,
			0
		)

		var closed = (deviceCount > 20)
		deviceGroups.forEach((group) => group.setAttribute('closed', closed))

		this.setState({ deviceGroups })
		this.setDeviceGroups(deviceGroups)
	}
	setDeviceGroups (deviceGroups) {
		this.setURLFilter(deviceGroups)
		this.props.onSetShared('deviceGroups', deviceGroups)
	}
	getDeviceGroups () {
		return this.state.deviceGroups
	}
	getURLFilter (enabled = true) {
		if ( ! enabled) {
			return { includeByDefault: true, deviceGroups: {}, devices: {} }
		}

		var { allDevices = 1, deviceGroups = '', devices = '' } = this.context.location.query
		var includeByDefault = !! parseInt(allDevices)
		var create = (items) => _.chain(items.split(','))
			.filter((id) => id)
			.map((id) => [id, ! includeByDefault])
			.object()
			.value()

		deviceGroups = create(deviceGroups)
		devices = create(devices)

		return { includeByDefault, deviceGroups, devices }
	}
	setURLFilter (deviceGroups) {
		// Create shortest possible query parameters
		// Determine filtering all
		var averages = _.chain(deviceGroups)
			.map((group) => _.filter(group.getDevices(), (device) => device.getAttribute('include')).length / group.getDevices().length)
			.value()
		var sum = _.reduce(averages, (memo, num) => memo + num, 0)
		var allDevices = (sum > 0 && sum / averages.length >= 0.5)

		// Determine device group and device filters
		var filteredDeviceGroups = []
		var filteredDevices = []

		deviceGroups.forEach((group, i) => {
			if (averages[i] === (allDevices ? 0 : 1)) {
				filteredDeviceGroups.push(group.id)
			} else {
				// Determine device filters
				group.getDevices().forEach((device) => {
					if (device.getAttribute('include') !== allDevices) {
						filteredDevices.push(device.id)
					}
				})
			}
		})

		var location = this.context.location
		var query = Object.assign({}, location.query)
		query.allDevices = (allDevices ? 1 : 0)
		query.deviceGroups = (filteredDeviceGroups.join(',') || undefined)
		query.devices = (filteredDevices.join(',') || undefined)

		var route = Object.assign({}, location, {
			pathname: (location.pathname.startsWith('/') ? '' : '/') + location.pathname,
			query,
		})
		this.context.router.push(route)
	}
	onToggleAll () {
		var deviceGroups = this.getDeviceGroups()
		var include = ! this.getInfo(deviceGroups).allIncluded
		deviceGroups.map((group) => {
			group.getDevices().forEach((device) => {
				device.setAttribute('include', include)
			})
		})
		this.setDeviceGroups(deviceGroups)
	}
	onDeviceGroupChange (group, include) {
		var deviceGroups = this.getDeviceGroups()
		var deviceGroup = deviceGroups[deviceGroups.indexOf(group)]
		if ( ! deviceGroup) throw new Error('No such device group.')
		deviceGroup.getDevices().forEach((device) => {
			device.setAttribute('include', include)
		})
		this.setDeviceGroups(deviceGroups)
	}
	onDeviceChange (group, device, include) {
		var deviceGroups = this.getDeviceGroups()
		var deviceGroup = deviceGroups[deviceGroups.indexOf(group)]
		if ( ! deviceGroup) throw new Error('No such device group.')
		var devices = deviceGroup.getDevices()
		device = devices[devices.indexOf(device)]
		if ( ! device) throw new Error('No such device.')
		device.setAttribute('include', include)
		this.setDeviceGroups(deviceGroups)
	}
	getInfo (deviceGroups) {
		var devices
		if (Array.isArray(deviceGroups)) {
			devices = _.chain(deviceGroups)
				.map((group) => group.getDevices())
				.flatten()
				.uniq(false, (device) => device.id)
				.value()
		} else {
			// Single device group
			devices = deviceGroups.getDevices()
		}

		var includedDevices = devices.filter((device) => device.getAttribute('include'))
		var allIncluded = (includedDevices.length === devices.length)
		var labelClass = 'label-default'

		if (allIncluded) {
			labelClass = 'label-success'
		} else if (includedDevices.length > 0) {
			labelClass = 'label-warning'
		}

		var Label = (

				{includedDevices.length} / {devices.length}

		)

		return { allIncluded, Label }
	}

	render () {
		var deviceGroups = this.getDeviceGroups()

		if (this.props.isFetching || ! deviceGroups) {
			return (
Loading…
)
		}

		var deviceLabels = user.getConfig('deviceLabels')
		var info = this.getInfo(deviceGroups)

		return (

					<Checkbox
						style={{ marginLeft: (this.props.groupsOnly ? 0 : 34), marginRight: 8 }}
						onChange={() => this.onToggleAll()}
						checked={info.allIncluded} />
					{' All'}
					{info.Label}

			{deviceGroups.map((group, i) => {
				var info = this.getInfo(group)
				var closed = !! group.getAttribute('closed')

				return (

						{ ! this.props.groupsOnly && (
							<button class="btn btn-xs btn-default" style={{ marginRight: 10 }} onClick={() => {
								group.setAttribute('closed', ! closed)
								this.forceUpdate()
							}}>

							</button>
						)}
						<Checkbox
							style={{ marginRight: 8 }}
							onChange={(ev) => this.onDeviceGroupChange(group, ev.target.checked)}
							checked={info.allIncluded} />
						{' ' + group.name}
						{info.Label}
						{ ! this.props.groupsOnly
							&& group.getDevices().length > 0
							&& ! closed
							&& (

							{group.getDevices().map((device, i) => (

										<Checkbox
											style={{ marginRight: 8 }}
											onChange={(ev) => this.onDeviceChange(group, device, ev.target.checked)}
											checked={device.getAttribute('include')} />
										<span title={`${device.name} (${device.id})`}>
											{' ' + (deviceLabels[device.id] || device.name)}

							))}

						)}

				)
			})}

		)
	}
}

DevicePickerWidget.propTypes = Object.assign({}, BaseWidget.propTypes, {
	onFetchAll: React.PropTypes.func.isRequired,
	isFetching: React.PropTypes.bool.isRequired,
	items: React.PropTypes.array,
	fromURL: React.PropTypes.bool,
	groupsOnly: React.PropTypes.bool,
	shared: React.PropTypes.shape({
		deviceGroups: React.PropTypes.array,
	}).isRequired,
})
DevicePickerWidget.contextTypes = {
	location: React.PropTypes.object,
	router: React.PropTypes.object,
}

export default connect(
	mapStateToProps,
	mapDispatchToProps
)(DevicePickerWidget)
<commit_msg>Fix device labels in device picker widget.<commit_after>import _ from 'underscore'
import React from 'react'
import { connect } from 'react-redux'
import actions from '../../actions/deviceGroup'
import BaseWidget from './Base'
import { current as user } from '../../models/User'
import Checkbox from 'rc-checkbox'

import 'rc-checkbox/assets/index.css'

const mapStateToProps = (state) => {
	var { loading: isFetching, items } = state.deviceGroup.fetchAll
	return { isFetching, items }
}

const mapDispatchToProps = (dispatch) => {
	return {
		onFetchAll () {
			return dispatch(actions.fetchAll())
		},
	}
}

class DevicePickerWidget extends BaseWidget {
	constructor () {
		super()
		this.state = {
			deviceGroups: null,
		}
	}
	componentWillMount () {
		if (this.props.shared.deviceGroups || this.props.items) {
			this.onDeviceGroups(this.props)
		} else {
			this.props.onFetchAll()
		}
	}
	componentWillReceiveProps (next) {
		var deviceGroups = (next.shared.deviceGroups || next.items)

		if (deviceGroups && deviceGroups !== this.state.deviceGroups) {
			this.onDeviceGroups(next)
		}
	}
	onDeviceGroups (props) {
		// TODO(mauvm): Convert deviceGroups to model (e.g. filter.getUDIDs())
		// and set shared prop 'deviceFilter'

		var deviceGroups = (props.shared.deviceGroups || props.items)
		var filter = this.getURLFilter(props.fromURL)

		deviceGroups.forEach((group) => {
			var includeGroup = filter.deviceGroups[group.id]

			if (includeGroup === undefined) {
				includeGroup = filter.includeByDefault
			}

			group.getDevices().forEach((device) => {
				var includeDevice

				if ( ! props.groupsOnly) {
					includeDevice = filter.devices[device.id]
				}

				var include = (includeDevice !== undefined ? includeDevice : includeGroup)
				device.setAttribute('include', include)
			})
		})

		var deviceCount = _.reduce(
			deviceGroups.map((group) => group.getDevices().length),
			(memo, num) => memo + num,
			0
		)

		var closed = (deviceCount > 20)
		deviceGroups.forEach((group) => group.setAttribute('closed', closed))

		this.setState({ deviceGroups })
		this.setDeviceGroups(deviceGroups)
	}
	setDeviceGroups (deviceGroups) {
		this.setURLFilter(deviceGroups)
		this.props.onSetShared('deviceGroups', deviceGroups)
	}
	getDeviceGroups () {
		return this.state.deviceGroups
	}
	getURLFilter (enabled = true) {
		if ( ! enabled) {
			return { includeByDefault: true, deviceGroups: {}, devices: {} }
		}

		var { allDevices = 1, deviceGroups = '', devices = '' } = this.context.location.query
		var includeByDefault = !! parseInt(allDevices)
		var create = (items) => _.chain(items.split(','))
			.filter((id) => id)
			.map((id) => [id, ! includeByDefault])
			.object()
			.value()

		deviceGroups = create(deviceGroups)
		devices = create(devices)

		return { includeByDefault, deviceGroups, devices }
	}
	setURLFilter (deviceGroups) {
		// Create shortest possible query parameters
		// Determine filtering all
		var averages = _.chain(deviceGroups)
			.map((group) => _.filter(group.getDevices(), (device) => device.getAttribute('include')).length / group.getDevices().length)
			.value()
		var sum = _.reduce(averages, (memo, num) => memo + num, 0)
		var allDevices = (sum > 0 && sum / averages.length >= 0.5)

		// Determine device group and device filters
		var filteredDeviceGroups = []
		var filteredDevices = []

		deviceGroups.forEach((group, i) => {
			if (averages[i] === (allDevices ? 0 : 1)) {
				filteredDeviceGroups.push(group.id)
			} else {
				// Determine device filters
				group.getDevices().forEach((device) => {
					if (device.getAttribute('include') !== allDevices) {
						filteredDevices.push(device.id)
					}
				})
			}
		})

		var location = this.context.location
		var query = Object.assign({}, location.query)
		query.allDevices = (allDevices ? 1 : 0)
		query.deviceGroups = (filteredDeviceGroups.join(',') || undefined)
		query.devices = (filteredDevices.join(',') || undefined)

		var route = Object.assign({}, location, {
			pathname: (location.pathname.startsWith('/') ? '' : '/') + location.pathname,
			query,
		})
		this.context.router.push(route)
	}
	onToggleAll () {
		var deviceGroups = this.getDeviceGroups()
		var include = ! this.getInfo(deviceGroups).allIncluded
		deviceGroups.map((group) => {
			group.getDevices().forEach((device) => {
				device.setAttribute('include', include)
			})
		})
		this.setDeviceGroups(deviceGroups)
	}
	onDeviceGroupChange (group, include) {
		var deviceGroups = this.getDeviceGroups()
		var deviceGroup = deviceGroups[deviceGroups.indexOf(group)]
		if ( ! deviceGroup) throw new Error('No such device group.')
		deviceGroup.getDevices().forEach((device) => {
			device.setAttribute('include', include)
		})
		this.setDeviceGroups(deviceGroups)
	}
	onDeviceChange (group, device, include) {
		var deviceGroups = this.getDeviceGroups()
		var deviceGroup = deviceGroups[deviceGroups.indexOf(group)]
		if ( ! deviceGroup) throw new Error('No such device group.')
		var devices = deviceGroup.getDevices()
		device = devices[devices.indexOf(device)]
		if ( ! device) throw new Error('No such device.')
		device.setAttribute('include', include)
		this.setDeviceGroups(deviceGroups)
	}
	getInfo (deviceGroups) {
		var devices
		if (Array.isArray(deviceGroups)) {
			devices = _.chain(deviceGroups)
				.map((group) => group.getDevices())
				.flatten()
				.uniq(false, (device) => device.id)
				.value()
		} else {
			// Single device group
			devices = deviceGroups.getDevices()
		}

		var includedDevices = devices.filter((device) => device.getAttribute('include'))
		var allIncluded = (includedDevices.length === devices.length)
		var labelClass = 'label-default'

		if (allIncluded) {
			labelClass = 'label-success'
		} else if (includedDevices.length > 0) {
			labelClass = 'label-warning'
		}

		var Label = (

				{includedDevices.length} / {devices.length}

		)

		return { allIncluded, Label }
	}

	render () {
		var deviceGroups = this.getDeviceGroups()

		if (this.props.isFetching || ! deviceGroups) {
			return (
Loading…
)
		}

		var deviceLabels = user.getConfig('deviceLabels') || {}
		var info = this.getInfo(deviceGroups)

		return (

					<Checkbox
						style={{ marginLeft: (this.props.groupsOnly ? 0 : 34), marginRight: 8 }}
						onChange={() => this.onToggleAll()}
						checked={info.allIncluded} />
					{' All'}
					{info.Label}

			{deviceGroups.map((group, i) => {
				var info = this.getInfo(group)
				var closed = !! group.getAttribute('closed')

				return (

						{ ! this.props.groupsOnly && (
							<button class="btn btn-xs btn-default" style={{ marginRight: 10 }} onClick={() => {
								group.setAttribute('closed', ! closed)
								this.forceUpdate()
							}}>

							</button>
						)}
						<Checkbox
							style={{ marginRight: 8 }}
							onChange={(ev) => this.onDeviceGroupChange(group, ev.target.checked)}
							checked={info.allIncluded} />
						{' ' + group.name}
						{info.Label}
						{ ! this.props.groupsOnly
							&& group.getDevices().length > 0
							&& ! closed
							&& (

							{group.getDevices().map((device, i) => (

										<Checkbox
											style={{ marginRight: 8 }}
											onChange={(ev) => this.onDeviceChange(group, device, ev.target.checked)}
											checked={device.getAttribute('include')} />
										<span title={`${device.name} (${device.id})`}>
											{' ' + (deviceLabels[device.id] || device.name)}

							))}

						)}

				)
			})}

		)
	}
}

DevicePickerWidget.propTypes = Object.assign({}, BaseWidget.propTypes, {
	onFetchAll: React.PropTypes.func.isRequired,
	isFetching: React.PropTypes.bool.isRequired,
	items: React.PropTypes.array,
	fromURL: React.PropTypes.bool,
	groupsOnly: React.PropTypes.bool,
	shared: React.PropTypes.shape({
		deviceGroups: React.PropTypes.array,
	}).isRequired,
})
DevicePickerWidget.contextTypes = {
	location: React.PropTypes.object,
	router: React.PropTypes.object,
}

export default connect(
	mapStateToProps,
	mapDispatchToProps
)(DevicePickerWidget)
<|endoftext|>


<commit_before>'use strict';

const debug = require('debug')('wasted:map-directories');

const getStats = require('./get-stats');

module.exports = function(dirInfo) {
  debug();
  dirInfo.map(dir => {
    return new Promise((resolve, reject) => {
      getStats(dir.path)
      .then(resolve)
      .catch(reject);
    });
  });
};
<commit_msg>deleted unnecessary file<commit_after><|endoftext|>


<commit_before>module.exports = ctx => ({
  map: ctx.options.map,
  plugins: {
    "postcss-import": {},
    "postcss-cssnext": {},
    "cssnano": ctx.env === 'production' ? {autoprefixer: false} : false
  }
});
<commit_msg>Only show source maps in dev<commit_after>module.exports = ctx => {
  return {
    map: ctx.env === 'production' ? false : ctx.options.map, // source maps
    plugins: {
      "postcss-import": {},
      "postcss-cssnext": {},
      "cssnano": ctx.env === 'production' ? {autoprefixer: false} : false
    }
  }
};
<|endoftext|>


<commit_before>'use strict';
var util = require('util'),
    generators = require('yeoman-generator'),
    chalk = require('chalk'),
    scriptBase = require('../generator-base'),
    fs = require('fs'),
    shelljs = require('shelljs'),
    semver = require('semver');

var UpgradeGenerator = generators.Base.extend({});

util.inherits(UpgradeGenerator, scriptBase);

/* Constants used throughout */
const GENERATOR_JHIPSTER = 'generator-jhipster',
    UPGRADE_BRANCH = 'jhipster_upgrade';

module.exports = UpgradeGenerator.extend({
    constructor: function () {
        generators.Base.apply(this, arguments);
        this.force = this.options['force'];
    },

    initializing: {
        displayLogo: function () {
            this.log(chalk.green('Welcome to the JHipster Upgrade Sub-Generator '));
            this.log(chalk.green('This will upgrade your current application codebase to the latest JHipster version'));
        },

        getCurrentJHVersion: function () {
            this.currentVersion = this.config.get('jhipsterVersion');
        }
    },

    _gitCheckout: function(branch) {
        var done = this.async();
        this.gitExec(['checkout', '-q', branch], function(code, msg, err) {
            if (code !== 0) this.error('Unable to checkout branch ' + branch + ':\n' + err);
            this.log('Checked out branch \'' + branch + '\'');
            done();
        }.bind(this));
    },

    _gitCommitAll: function(commitMsg, callback) {
        var commit = function() {
            this.gitExec(['commit', '-q', '-m', '\"' + commitMsg + '\"', '-a', '--allow-empty'], function(code, msg, err) {
                if (code !== 0) this.error('Unable to commit in git:\n' + err);
                this.log('Committed: ' + commitMsg);
                callback();
            }.bind(this));
        }.bind(this);
        this.gitExec(['add', '-A'], {maxBuffer: 1024 * 500}, function(code, msg, err) {
            if (code !== 0) this.error('Unable to add resources in git:\n' + err);
            commit();
        }.bind(this));
    },

    configuring: {
        assertGitPresent: function() {
            var done = this.async();
            this.isGitInstalled(function (code) {
                if (code !== 0) this.error('Exiting the process.');
                done();
            }.bind(this));
        },

        checkLatestVersion: function() {
            this.log('Looking for latest ' + GENERATOR_JHIPSTER + ' version...');
            var done = this.async();
            shelljs.exec('npm show ' + GENERATOR_JHIPSTER + ' version', {silent:true}, function (code, msg, err) {
                this.latestVersion = msg.replace('\n','');
                if (semver.lt(this.currentVersion, this.latestVersion)) {
                    this.log(chalk.green('New ' + GENERATOR_JHIPSTER + ' version found: ' + this.latestVersion));
                } else if (this.force) {
                    this.log(chalk.yellow('Forced re-generation'));
                } else {
                    this.error(chalk.green('No update available.') + ' Application has already been generated with latest version.');
                }
                done();
            }.bind(this));
        },

        assertGitRepository: function() {
            if (! fs.existsSync('.git')) {
                var done = this.async();
                this.gitExec('init', function(code, msg, err) {
                    if (code !== 0) this.error('Unable to initialize a new git repository:\n' + err);
                    this.log('Initialized a new git repository');
                    this._gitCommitAll('Initial', function() {
                        done();
                    });
                }.bind(this));
            }
        },

        assertNoLocalChanges: function() {
            var done = this.async();
            this.gitExec(['status', '--porcelain'], function(code, msg, err) {
                if (code !== 0) this.error('Unable to check for local changes:\n' + err);
                if (msg != null && msg !== '') {
                    this.warning(' local changes found.\n' +
                        '\tPlease commit/stash them before upgrading');
                    this.error('Exiting process');
                }
                done();
            }.bind(this));
        },

        detectCurrentBranch: function() {
            var done = this.async();
            this.gitExec(['rev-parse', '-q', '--abbrev-ref', 'HEAD'], function(code, msg, err) {
                if (code !== 0) this.error('Unable to detect current git branch:\n' + err);
                this.sourceBranch = msg.replace('\n','');
                done();
            }.bind(this));
        },

        prepareUpgradeBranch: function() {
            var done = this.async();
            var createUpgradeBranch = function(callback) {
                this.gitExec(['branch', '-q', UPGRADE_BRANCH], function(code, msg, err) {
                    if (code !== 0) this.error('Unable to create ' + UPGRADE_BRANCH + ':\n' + err);
                    this.log('Created branch ' + UPGRADE_BRANCH);
                    this._gitCheckout(UPGRADE_BRANCH);
                    callback();
                }.bind(this));
            }.bind(this);
            this.gitExec(['rev-parse', '-q', '--verify', UPGRADE_BRANCH], function(code, msg, err) {
                if (code !== 0) createUpgradeBranch(done);
                else done();
            }.bind(this));
        }
    },

    default: {
        insight: function () {
            var insight = this.insight();
            insight.trackWithEvent('generator', 'upgrade');
        },

        updateJhipster: function() {
            this.log('Updating ' + GENERATOR_JHIPSTER + '. This might take some time...');
            var done = this.async();
            shelljs.exec('npm install -g ' + GENERATOR_JHIPSTER, {silent:true}, function (code, msg, err) {
                if (code === 0) this.log(chalk.green('Updated ' + GENERATOR_JHIPSTER + ' to version ' + this.latestVersion));
                else this.error('Something went wrong while updating generator! ' + err);
                done();
            }.bind(this));
        },

        cleanUp: function() {
            var done = this.async();
            if (shelljs.rm('-rf', '!(.yo-rc.json|.git)').code === 0 ) {
                this.log('Cleaned up directory');
            }
            done();
        },

        generate: function() {
            this.log('Regenerating app with jhipster ' + this.latestVersion + '...');
            var done = this.async();
            shelljs.exec('yo jhipster --force --with-entities', {silent:false}, function (code, msg, err) {
                if (code === 0) this.log(chalk.green('Successfully regenerated app with jhipster ' + this.latestVersion));
                else this.error('Something went wrong while generating project! '+ err);
                done();
            }.bind(this));
        },

        commitChanges: function() {
            var done = this.async();
            this._gitCommitAll('Upgrade to JHipster ' + this.latestVersion, function() {
                done();
            });
        },

        checkoutSourceBranch: function() {
            this._gitCheckout(this.sourceBranch);
        },

        mergeChangesBack: function() {
            this.log('Merging changes back to ' + this.sourceBranch + '...');
            var done = this.async();
            this.gitExec(['merge', '-q', UPGRADE_BRANCH], function(code, msg, err) {
                if (code !== 0) this.error('Unable to merge changes back to ' + this.sourceBranch + ':\n' + err);
                this.log(chalk.green('Merge done !') + '\n\tPlease now fix conflicts if any, and commit !');
                done();
            }.bind(this));
        }
    },

    end: function () {
        this.log(chalk.green.bold('\nUpgraded successfully.\n'));
    }

});
<commit_msg>polish<commit_after>'use strict';
var util = require('util'),
    generators = require('yeoman-generator'),
    chalk = require('chalk'),
    scriptBase = require('../generator-base'),
    fs = require('fs'),
    shelljs = require('shelljs'),
    semver = require('semver');

var UpgradeGenerator = generators.Base.extend({});

util.inherits(UpgradeGenerator, scriptBase);

/* Constants used throughout */
const GENERATOR_JHIPSTER = 'generator-jhipster',
    UPGRADE_BRANCH = 'jhipster_upgrade';

module.exports = UpgradeGenerator.extend({
    constructor: function () {
        generators.Base.apply(this, arguments);
        this.force = this.options['force'];
    },

    initializing: {
        displayLogo: function () {
            this.log(chalk.green('Welcome to the JHipster Upgrade Sub-Generator '));
            this.log(chalk.green('This will upgrade your current application codebase to the latest JHipster version'));
        },

        getCurrentJHVersion: function () {
            this.currentVersion = this.config.get('jhipsterVersion');
        }
    },

    _gitCheckout: function(branch) {
        var done = this.async();
        this.gitExec(['checkout', '-q', branch], function(code, msg, err) {
            if (code !== 0) this.error('Unable to checkout branch ' + branch + ':\n' + err);
            this.log('Checked out branch \'' + branch + '\'');
            done();
        }.bind(this));
    },

    _gitCommitAll: function(commitMsg, callback) {
        var commit = function() {
            this.gitExec(['commit', '-q', '-m', '\"' + commitMsg + '\"', '-a', '--allow-empty'], function(code, msg, err) {
                if (code !== 0) this.error('Unable to commit in git:\n' + err);
                this.log('Committed: ' + commitMsg);
                callback();
            }.bind(this));
        }.bind(this);
        this.gitExec(['add', '-A'], {maxBuffer: 1024 * 500}, function(code, msg, err) {
            if (code !== 0) this.error('Unable to add resources in git:\n' + err);
            commit();
        }.bind(this));
    },

    configuring: {
        assertGitPresent: function() {
            var done = this.async();
            this.isGitInstalled(function (code) {
                if (code !== 0) this.error('Exiting the process.');
                done();
            }.bind(this));
        },

        checkLatestVersion: function() {
            this.log('Looking for latest ' + GENERATOR_JHIPSTER + ' version...');
            var done = this.async();
            shelljs.exec('npm show ' + GENERATOR_JHIPSTER + ' version', {silent:true}, function (code, msg, err) {
                this.latestVersion = msg.replace('\n','');
                if (semver.lt(this.currentVersion, this.latestVersion)) {
                    this.log(chalk.green('New ' + GENERATOR_JHIPSTER + ' version found: ' + this.latestVersion));
                } else if (this.force) {
                    this.log(chalk.yellow('Forced re-generation'));
                } else {
                    this.error(chalk.green('No update available.') + ' Application has already been generated with latest version.');
                }
                done();
            }.bind(this));
        },

        assertGitRepository: function() {
            if (! fs.existsSync('.git')) {
                var done = this.async();
                this.gitExec('init', function(code, msg, err) {
                    if (code !== 0) this.error('Unable to initialize a new git repository:\n' + err);
                    this.log('Initialized a new git repository');
                    this._gitCommitAll('Initial', function() {
                        done();
                    });
                }.bind(this));
            }
        },

        assertNoLocalChanges: function() {
            var done = this.async();
            this.gitExec(['status', '--porcelain'], function(code, msg, err) {
                if (code !== 0) this.error('Unable to check for local changes:\n' + err);
                if (msg != null && msg !== '') {
                    this.warning(' local changes found.\n' +
                        '\tPlease commit/stash them before upgrading');
                    this.error('Exiting process');
                }
                done();
            }.bind(this));
        },

        detectCurrentBranch: function() {
            var done = this.async();
            this.gitExec(['rev-parse', '-q', '--abbrev-ref', 'HEAD'], function(code, msg, err) {
                if (code !== 0) this.error('Unable to detect current git branch:\n' + err);
                this.sourceBranch = msg.replace('\n','');
                done();
            }.bind(this));
        },

        prepareUpgradeBranch: function() {
            var done = this.async();
            var createUpgradeBranch = function(callback) {
                this.gitExec(['branch', '-q', UPGRADE_BRANCH], function(code, msg, err) {
                    if (code !== 0) this.error('Unable to create ' + UPGRADE_BRANCH + ':\n' + err);
                    this.log('Created branch ' + UPGRADE_BRANCH);
                    this._gitCheckout(UPGRADE_BRANCH);
                    callback();
                }.bind(this));
            }.bind(this);
            this.gitExec(['rev-parse', '-q', '--verify', UPGRADE_BRANCH], function(code, msg, err) {
                if (code !== 0) createUpgradeBranch(done);
                else done();
            }.bind(this));
        }
    },

    default: {
        insight: function () {
            var insight = this.insight();
            insight.trackWithEvent('generator', 'upgrade');
        },

        updateJhipster: function() {
            this.log('Updating ' + GENERATOR_JHIPSTER + '. This might take some time...');
            var done = this.async();
            shelljs.exec('npm install -g ' + GENERATOR_JHIPSTER, {silent:true}, function (code, msg, err) {
                if (code === 0) this.log(chalk.green('Updated ' + GENERATOR_JHIPSTER + ' to version ' + this.latestVersion));
                else this.error('Something went wrong while updating generator! ' + err);
                done();
            }.bind(this));
        },

        cleanUp: function() {
            var done = this.async();
            if (shelljs.rm('-rf', '!(.yo-rc.json|.git)').code === 0 ) {
                this.log('Cleaned up directory');
            }
            done();
        },

        generate: function() {
            this.log('Regenerating app with jhipster ' + this.latestVersion + '...');
            var done = this.async();
            shelljs.exec('yo jhipster --with-entities --force', {silent:false}, function (code, msg, err) {
                if (code === 0) this.log(chalk.green('Successfully regenerated app with jhipster ' + this.latestVersion));
                else this.error('Something went wrong while generating project! '+ err);
                done();
            }.bind(this));
        },

        commitChanges: function() {
            var done = this.async();
            this._gitCommitAll('Upgrade to JHipster ' + this.latestVersion, function() {
                done();
            });
        },

        checkoutSourceBranch: function() {
            this._gitCheckout(this.sourceBranch);
        },

        mergeChangesBack: function() {
            this.log('Merging changes back to ' + this.sourceBranch + '...');
            var done = this.async();
            this.gitExec(['merge', '-q', UPGRADE_BRANCH], function(code, msg, err) {
                if (code !== 0) this.error('Unable to merge changes back to ' + this.sourceBranch + ':\n' + err);
                this.log(chalk.green('Merge done !') + '\n\tPlease now fix conflicts if any, and commit !');
                done();
            }.bind(this));
        }
    },

    end: function () {
        this.log(chalk.green.bold('\nUpgraded successfully.\n'));
    }

});
<|endoftext|>


<commit_before>
"use strict"

var createPushbackStream = function(originalReadStream, pushbackBuffers) {
  var readStream = Object.create(originalReadStream)

  var reading = false
  var unoverrideMethods = function() {
    delete readStream.read
    delete readStream.closeRead
  }

  readStream.read = function(callback) {
    if(reading) throw new Error(
      'read called multiple times without waiting for callback')

    if(pushbackBuffers.length == 0) {
      unoverrideMethods()
      originalReadStream.read(callback)
    } else {
      reading = true
      process.nextTick(function() {
        reading = false
        callback(null, pushbackBuffers.shift())
      })
    }
  }

  readStream.closeRead = function(err) {
    originalReadStream.closeRead(err)
    unoverrideMethods()
  }

  return readStream
}

module.exports = {
  createPushbackStream: createPushbackStream
}<commit_msg>Directly replace readStream methods instead of creating prototype.<commit_after>
"use strict"

var createPushbackStream = function(readStream, pushbackBuffers) {
  var reading = false
  var originalRead = readStream.read
  var originalCloseRead = readStream.closeRead

  var unoverrideMethods = function() {
    readStream.read = originalRead
    readStream.closeRead = originalCloseRead
  }

  readStream.read = function(callback) {
    if(reading) throw new Error(
      'read called multiple times without waiting for callback')

    if(pushbackBuffers.length == 0) {
      unoverrideMethods()
      originalRead(callback)
    } else {
      reading = true
      process.nextTick(function() {
        reading = false
        callback(null, pushbackBuffers.shift())
      })
    }
  }

  readStream.closeRead = function(err) {
    originalCloseRead(err)
    unoverrideMethods()
  }

  return readStream
}

module.exports = {
  createPushbackStream: createPushbackStream
}<|endoftext|>


<commit_before>/* @flow */

import {NativeProcess} from './NativeProcess';
import {stat} from 'fs';
import {join} from 'path';
import noop from 'lodash/noop';
import {logError} from './logger';

const npm = new NativeProcess('npm'),
  cwd = process.cwd(),
  defaultOptions = {
    inputDir: join(cwd, 'src'),
    outputDir: join(cwd, 'docs'),
    readMe: join(cwd, 'README.md'),
    template: join(cwd, 'node_modules', 'docdash'),
    jsdocConfig: join(__dirname, '..', 'config', 'jsdoc.json')
  };

/**
 * Generates API documentation
 *
 * The default JSDoc plugin specified in `jsdocConfig` strips out all of the code from a file while retaining newlines
 * (unlike the built in `commentsOnly` plugin that ships with JSDoc3).
 *
 * That way:
 * 1. line numbers are preserved in the source view
 * 2. you don't need to use a pre-compiler, you will always see the same code as you wrote in the docs source view
 * 3. since JSDoc only sees comments you can use any code syntax you like - ES2015, ES7, JSX, it doesn't even have to be
 * JavaScript.
 *
 * The markdown plugin is also included by default.
 *
 * @class Documentation
 * @param {DocumentationConfig} [config={}] - a configuration object
 * @example
 * import {Documentation} from 'webcompiler';
 * // or - import {Documentation} from 'webcompiler/lib/Documentation';
 * // or - var Documentation = require('webcompiler').Documentation;
 * // or - var Documentation = require('webcompiler/lib/Documentation').Documentation;
 *
 * const docs = new Documentation();
 */
export class Documentation {
  /**
   * JSDoc3
   *
   * @member {NativeProcess} jsdoc
   * @memberof Documentation
   * @private
   * @instance
   */
  jsdoc: ?NativeProcess;

  /**
   * documentation generator configuration object
   *
   * @member {DocumentationConfig} options
   * @memberof Documentation
   * @private
   * @instance
   */
  options: Object;

  /* eslint-disable require-jsdoc */
  constructor(options: Object = {}) {
    /* eslint-enable require-jsdoc */
    this.options = {...defaultOptions, ...options};
  }

  /**
   * Finds a path to the JSDoc3 executable
   *
   * @memberof Documentation
   * @static
   * @private
   * @method findExecutable
   * @param {Function} callback - a callback function
   * @example
   * Documentation.findExecutable(file => {
   *   // the jsdoc file is found
   * });
   */
  static findExecutable(callback: Function) {
    Documentation.checkBin(localFile => {
      if (localFile) {
        return callback(localFile);
      }
      Documentation.checkBin(globalFile => {
        if (globalFile) {
          return callback(globalFile);
        }
        console.error('Failed to locate the jsdoc executable');
      }, true);
    });
  }

  /**
   * Checks the NPM bin directories to see if they contain a file named jsdoc
   *
   * @memberof Documentation
   * @static
   * @private
   * @method checkBin
   * @param {Function} callback              - a callback function
   * @param {boolean}  [globalPackage=false] - if true checks the global NPM bin directory (contains the npm executable
   *                                           itself)
   * @example
   * Documentation.checkBin(file => {
   *   if (file) {
   *     // the jsdoc file is found
   *   }
   * });
   */
  static checkBin(callback: Function, globalPackage: boolean = false) {
    const args = ['bin'];

    if (globalPackage) {
      args.push('-g');
    }
    npm.run((stderr, stdout) => {
      if (stderr) {
        console.error(stderr);

        return callback(null);
      }
      const path = join(stdout.replace(/\n$/, ''), 'jsdoc');

      stat(path, err => {
        callback(err ? null : path);
      });
    }, args);
  }

  /**
   * Generate the documentation
   *
   * @memberof Documentation
   * @instance
   * @method run
   * @param {Function} [callback=function () {}] - a callback function
   * @return {void}
   * @example
   * docs.run(() => {
   *   // generated the API documentation
   * });
   */
  run(callback: () => void = noop) {
    if (this.jsdoc) {
      return this.doRun(this.jsdoc, callback);
    }
    Documentation.findExecutable(file => {
      this.jsdoc = new NativeProcess(file);
      this.doRun(this.jsdoc, callback);
    });
  }

  /**
   * Given a JSDoc3 executable, generate the documentation
   *
   * @memberof Documentation
   * @instance
   * @private
   * @method doRun
   * @param {NativeProcess} jsdoc    - JSDoc3
   * @param {Function}      callback - a callback function
   * @example
   * docs.doRun(jsdoc, () => {
   *   // generated the API documentation
   * });
   */
  doRun(jsdoc: NativeProcess, callback: () => void) {
    const {inputDir, outputDir, readMe, template, jsdocConfig} = this.options;

    jsdoc.run(stderr => {
      if (stderr) {
        return logError(stderr);
      }
      callback();
    }, [inputDir, '-d', outputDir, '-R', readMe, '-c', jsdocConfig, '-t', template]);
  }

}
<commit_msg>[changed] `Documentation` only uses the local project level `jsdoc` executable, and throws if it is not found ignoring the global one, for a more fine-grained control over versioning<commit_after>/* @flow */

import {NativeProcess} from './NativeProcess';
import {stat} from 'fs';
import {join} from 'path';
import noop from 'lodash/noop';
import {logError} from './logger';

const npm = new NativeProcess('npm'),
  cwd = process.cwd(),
  defaultOptions = {
    inputDir: join(cwd, 'src'),
    outputDir: join(cwd, 'docs'),
    readMe: join(cwd, 'README.md'),
    template: join(cwd, 'node_modules', 'docdash'),
    jsdocConfig: join(__dirname, '..', 'config', 'jsdoc.json')
  };

/**
 * Generates API documentation
 *
 * The default JSDoc plugin specified in `jsdocConfig` strips out all of the code from a file while retaining newlines
 * (unlike the built in `commentsOnly` plugin that ships with JSDoc3).
 *
 * That way:
 * 1. line numbers are preserved in the source view
 * 2. you don't need to use a pre-compiler, you will always see the same code as you wrote in the docs source view
 * 3. since JSDoc only sees comments you can use any code syntax you like - ES2015, ES7, JSX, it doesn't even have to be
 * JavaScript.
 *
 * The markdown plugin is also included by default.
 *
 * @class Documentation
 * @param {DocumentationConfig} [config={}] - a configuration object
 * @example
 * import {Documentation} from 'webcompiler';
 * // or - import {Documentation} from 'webcompiler/lib/Documentation';
 * // or - var Documentation = require('webcompiler').Documentation;
 * // or - var Documentation = require('webcompiler/lib/Documentation').Documentation;
 *
 * const docs = new Documentation();
 */
export class Documentation {

  /**
   * JSDoc3
   *
   * @member {NativeProcess} jsdoc
   * @memberof Documentation
   * @private
   * @instance
   */
  jsdoc: ?NativeProcess;

  /**
   * documentation generator configuration object
   *
   * @member {DocumentationConfig} options
   * @memberof Documentation
   * @private
   * @instance
   */
  options: Object;

  /* eslint-disable require-jsdoc */
  constructor(options: Object = {}) {
    /* eslint-enable require-jsdoc */
    this.options = {...defaultOptions, ...options};
  }

  /**
   * Finds a path to the project level JSDoc3 executable
   *
   * @memberof Documentation
   * @static
   * @private
   * @method findExecutable
   * @param {Function} callback - a callback function
   */
  static findExecutable(callback: Function) {
    npm.run((stderr, stdout) => {
      if (stderr) {
        return logError(stderr);
      }
      const path = join(stdout.trimRight(), 'jsdoc');

      stat(path, statErr => {
        if (statErr) {
          return logError(statErr);
        }
        callback(path);
      });
    }, ['bin']);
  }

  /**
   * Generate the documentation
   *
   * @memberof Documentation
   * @instance
   * @method run
   * @param {Function} [callback=function () {}] - a callback function
   * @return {void}
   * @example
   * docs.run(() => {
   *   // generated the API documentation
   * });
   */
  run(callback: () => void = noop) {
    if (this.jsdoc) {
      return this.doRun(this.jsdoc, callback);
    }
    Documentation.findExecutable(file => {
      this.jsdoc = new NativeProcess(file);
      this.doRun(this.jsdoc, callback);
    });
  }

  /**
   * Given a JSDoc3 executable, generate the documentation
   *
   * @memberof Documentation
   * @instance
   * @private
   * @method doRun
   * @param {NativeProcess} jsdoc    - JSDoc3
   * @param {Function}      callback - a callback function
   * @example
   * docs.doRun(jsdoc, () => {
   *   // generated the API documentation
   * });
   */
  doRun(jsdoc: NativeProcess, callback: () => void) {
    const {inputDir, outputDir, readMe, template, jsdocConfig} = this.options;

    jsdoc.run(stderr => {
      if (stderr) {
        return logError(stderr);
      }
      callback();
    }, [inputDir, '-d', outputDir, '-R', readMe, '-c', jsdocConfig, '-t', template]);
  }

}
<|endoftext|>


<commit_before>import { all, spread } from 'axios';
import { getLoss } from 'services/analysis-cached';
import { getYearsRangeFromMinMax } from 'components/widgets/utils/data';

import {
  POLITICAL_BOUNDARIES_DATASET,
  FOREST_LOSS_DATASET,
  TREE_PLANTATIONS_DATASET,
} from 'data/datasets';
import {
  DISPUTED_POLITICAL_BOUNDARIES,
  POLITICAL_BOUNDARIES,
  FOREST_LOSS,
  TREE_PLANTATIONS,
} from 'data/layers';

import getWidgetProps from './selectors';

const MIN_YEAR = 2013;
const MAX_YEAR = 2020;

export default {
  widget: 'treeLossPlantations',
  title: 'Forest loss in natural forest in {location}',
  large: true,
  categories: ['forest-change'],
  types: ['country', 'aoi', 'wdpa'],
  admins: ['adm0', 'adm1', 'adm2'],
  settingsConfig: [
    {
      key: 'years',
      label: 'years',
      endKey: 'endYear',
      startKey: 'startYear',
      type: 'range-select',
      border: true,
    },
    {
      key: 'threshold',
      label: 'canopy density',
      type: 'mini-select',
      metaKey: 'widget_canopy_density',
    },
  ],
  refetchKeys: ['threshold'],
  chartType: 'composedChart',
  colors: 'loss',
  metaKey: 'widget_plantations_tree_cover_loss',
  datasets: [
    {
      dataset: POLITICAL_BOUNDARIES_DATASET,
      layers: [DISPUTED_POLITICAL_BOUNDARIES, POLITICAL_BOUNDARIES],
      boundary: true,
    },
    {
      // global plantations
      dataset: TREE_PLANTATIONS_DATASET,
      layers: [TREE_PLANTATIONS],
    },
    // loss
    {
      dataset: FOREST_LOSS_DATASET,
      layers: [FOREST_LOSS],
    },
  ],
  sortOrder: {
    forestChange: 2,
  },
  sentence:
    'From {startYear} to {endYear}, {percentage} of tree cover loss in {location} occurred within {lossPhrase}. The total loss within natural forest was equivalent to {value} of CO\u2082e emissions.',
  whitelists: {
    indicators: ['plantations'],
    checkStatus: true,
  },
  settings: {
    threshold: 30,
    startYear: MIN_YEAR,
    endYear: MAX_YEAR,
    extentYear: 2010,
  },
  getData: (params) =>
    all([
      getLoss({ ...params, forestType: 'plantations' }),
      getLoss({ ...params, forestType: '' }),
    ]).then(
      spread((plantationsloss, gadmLoss) => {
        let data = {};
        const lossPlantations =
          plantationsloss.data && plantationsloss.data.data;
        const totalLoss = gadmLoss.data && gadmLoss.data.data;
        if (
          lossPlantations &&
          totalLoss &&
          lossPlantations.length &&
          totalLoss.length
        ) {
          data = {
            lossPlantations,
            totalLoss,
          };
        }
        const { startYear, endYear, range } = getYearsRangeFromMinMax(
          MIN_YEAR,
          MAX_YEAR
        );
        return {
          ...data,
          settings: {
            startYear,
            endYear,
            yearsRange: range,
          },
          options: {
            years: range,
          },
        };
      })
    ),
  getDataURL: (params) => [
    getLoss({ ...params, forestType: 'plantations', download: true }),
    getLoss({ ...params, forestType: '', download: true }),
  ],
  getWidgetProps,
};
<commit_msg>Add 2021 to Forest loss in natural forest widget<commit_after>import { all, spread } from 'axios';
import { getLoss } from 'services/analysis-cached';
import { getYearsRangeFromMinMax } from 'components/widgets/utils/data';

import {
  POLITICAL_BOUNDARIES_DATASET,
  FOREST_LOSS_DATASET,
  TREE_PLANTATIONS_DATASET,
} from 'data/datasets';
import {
  DISPUTED_POLITICAL_BOUNDARIES,
  POLITICAL_BOUNDARIES,
  FOREST_LOSS,
  TREE_PLANTATIONS,
} from 'data/layers';

import getWidgetProps from './selectors';

const MIN_YEAR = 2013;
const MAX_YEAR = 2021;

export default {
  widget: 'treeLossPlantations',
  title: 'Forest loss in natural forest in {location}',
  large: true,
  categories: ['forest-change'],
  types: ['country', 'aoi', 'wdpa'],
  admins: ['adm0', 'adm1', 'adm2'],
  settingsConfig: [
    {
      key: 'years',
      label: 'years',
      endKey: 'endYear',
      startKey: 'startYear',
      type: 'range-select',
      border: true,
    },
    {
      key: 'threshold',
      label: 'canopy density',
      type: 'mini-select',
      metaKey: 'widget_canopy_density',
    },
  ],
  refetchKeys: ['threshold'],
  chartType: 'composedChart',
  colors: 'loss',
  metaKey: 'widget_plantations_tree_cover_loss',
  datasets: [
    {
      dataset: POLITICAL_BOUNDARIES_DATASET,
      layers: [DISPUTED_POLITICAL_BOUNDARIES, POLITICAL_BOUNDARIES],
      boundary: true,
    },
    {
      // global plantations
      dataset: TREE_PLANTATIONS_DATASET,
      layers: [TREE_PLANTATIONS],
    },
    // loss
    {
      dataset: FOREST_LOSS_DATASET,
      layers: [FOREST_LOSS],
    },
  ],
  sortOrder: {
    forestChange: 2,
  },
  sentence:
    'From {startYear} to {endYear}, {percentage} of tree cover loss in {location} occurred within {lossPhrase}. The total loss within natural forest was equivalent to {value} of CO\u2082e emissions.',
  whitelists: {
    indicators: ['plantations'],
    checkStatus: true,
  },
  settings: {
    threshold: 30,
    startYear: MIN_YEAR,
    endYear: MAX_YEAR,
    extentYear: 2010,
  },
  getData: (params) =>
    all([
      getLoss({ ...params, forestType: 'plantations' }),
      getLoss({ ...params, forestType: '' }),
    ]).then(
      spread((plantationsloss, gadmLoss) => {
        let data = {};
        const lossPlantations =
          plantationsloss.data && plantationsloss.data.data;
        const totalLoss = gadmLoss.data && gadmLoss.data.data;
        if (
          lossPlantations &&
          totalLoss &&
          lossPlantations.length &&
          totalLoss.length
        ) {
          data = {
            lossPlantations,
            totalLoss,
          };
        }
        const { startYear, endYear, range } = getYearsRangeFromMinMax(
          MIN_YEAR,
          MAX_YEAR
        );
        return {
          ...data,
          settings: {
            startYear,
            endYear,
            yearsRange: range,
          },
          options: {
            years: range,
          },
        };
      })
    ),
  getDataURL: (params) => [
    getLoss({ ...params, forestType: 'plantations', download: true }),
    getLoss({ ...params, forestType: '', download: true }),
  ],
  getWidgetProps,
};
<|endoftext|>