<commit_before>/* eslint capitalized-comments: ["off"],
          comma-dangle: ["off"] */

/**
 * @fileOverview Behavior-driven tests of the generator.
 *
 * @author Andy Culbertson
 *
 * @requires NPM:path
 * @requires NPM:yeoman-assert
 * @requires NPM:yeoman-test
 * @requires NPM:fs
 */

'use strict';
const path = require('path');
const assert = require('yeoman-assert');
const helpers = require('yeoman-test');
const fs = require('fs');
const process = require('process');
const childProcess = require('child_process');

/**
 * Return an absolute path converted from the given relative path.
 *
 * @param {String}   relPath An relative path.
 *
 * @returns {String} The absolute path based on the current working directory.
 */
var absPath = function (relPath) {
  return process.cwd() + '/' + relPath;
};

/**
 * Return an Array of absolute paths converted from the given relative paths.
 *
 * @param {Array}   relPaths An Array of relative paths.
 *
 * @returns {Array} The absolute paths based on the current working directory.
 */
var absPaths = function (relPaths) {
  return relPaths.map(function (relPath) {
    return absPath(relPath);
  });
};

// Adapted from https://gist.github.com/kethinov/6658166
/**
 * Return an array of files and directories found within dir, including the contents of its subdirectories.
 *
 * @param   {String}         dir      The relative path of the directory you're listing.
 * @param   {String}         dirBase  The absolute base for the relative path.
 * @param   {Array.<String>} itemList The list of paths to append to. This should usually be empty.
 * @param   {String}         itemType The type of filesystem item to return: 'all', 'dir', 'emptyDir', or 'file'.
 *
 * @returns {Array.<String>} The list of files and directories under dir.
 */
var walkSync = function (dir, dirBase, itemList, itemType) {
  const dirAbsPath = path.join(dirBase, dir);
  const theseItems = fs.readdirSync(dirAbsPath);
  itemList = itemList || [];
  if (theseItems.length === 0 && itemType === 'emptyDir') {
    itemList.push(dir);
  }

  theseItems.forEach(function (item) {
    const itemRelPath = path.join(dir, item);
    if (fs.statSync(path.join(dirAbsPath, item)).isDirectory()) {
      if (itemType === 'all' || itemType === 'dir') {
        itemList.push(itemRelPath);
      }

      itemList = walkSync(itemRelPath, dirBase, itemList, itemType);
    } else if (itemType === 'all' || itemType === 'file') {
      itemList.push(itemRelPath);
    }
  });
  return itemList;
};

/**
 * Replace '\' with '/' in the given list of paths.
 *
 * @param {Array} paths The paths with possible backslashes.
 *
 * @returns {Array} The paths with forward slashes.
 */
var normalizePathSeparators = function (paths) {
  return paths.map(function (path) {
    return path.replace(/\\/g, '/');
  });
};

// Adapted from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#Implementing_basic_set_operations
/**
 * Return the set difference between setA and setB.
 *
 * @param {Set}   setA The set with potentially extra elements.
 * @param {Set}   setB The set to subtract.
 *
 * @returns {Set} The elements in setA that aren't in setB.
 */
var difference = function (setA, setB) {
  var difference = new Set(setA);
  for (const elem of setB) {
    difference.delete(elem);
  }

  return difference;
};

const packageName = 'cookiefactory';

describe('generator-python-cmd:app', () => {
  beforeAll(() => {
    return helpers.run(path.join(__dirname, '../generators/app'))
      .withPrompts({projectName: 'Cookie Factory',
        packageName: packageName,
        projectDesc: 'A project for managing my coding projects',
        author: 'Andy Culbertson',
        authorEmail: 'thinkulum@gmail.com'});
  });

  const expectedFilePaths = [
    '.editorconfig',
    '.gitignore',
    'app.py',
    'LICENSE.md',
    'README.md',
    'setup.py',
    'requirements.txt',
    'requirements-dev.txt',
    packageName + '/cli.py',
    packageName + '/config.py',
    packageName + '/controller.py',
    packageName + '/__init__.py',
    'tests/context.py',
    'tests/test_app.py',
  ];

  const expectedEmptyDirPaths = [
    'config',
    'docs',
  ];

  it('creates expected files', () => {
    assert.file(absPaths(expectedFilePaths));
  });

  it('doesn\'t create unexpected files', () => {
    const actualFileSet = new Set(
      normalizePathSeparators(
        walkSync('', process.cwd(), [], 'file')));
    const expectedFileSet = new Set(expectedFilePaths);
    assert.equal(
      difference(actualFileSet, expectedFileSet).size,
      0);
  });

  it('creates expected empty directories', () => {
    assert.file(absPaths(expectedEmptyDirPaths));
  });

  it('doesn\'t create unexpected empty directories', () => {
    const actualEmptyDirSet = new Set(
      normalizePathSeparators(
        walkSync('', process.cwd(), [], 'emptyDir')));
    const expectedEmptyDirSet = new Set(expectedEmptyDirPaths);
    assert.equal(
      difference(actualEmptyDirSet, expectedEmptyDirSet).size,
      0);
  });

  it('creates expected file content', () => {
    var fileContentItems = [];
    var i;
    for (i = 0; i < expectedFilePaths.length; i++) {
      const expectedFilePath = expectedFilePaths[i];
      const contentFilePath = path.join(
        __dirname,
        'data',
        expectedFilePath.replace(
          /^\./, ''));
      const expectedContent = fs.readFileSync(
        contentFilePath, 'utf8', (err, data) => {
          if (err) {
            throw err;
          }

          return data;
        });

      fileContentItems.push(
        [absPath(expectedFilePath), expectedContent]);
    }

    assert.fileContent(fileContentItems);
  });
});
<commit_msg>Add generator test for passing the project tests<commit_after>/* eslint capitalized-comments: ["off"],
          comma-dangle: ["off"] */

/**
 * @fileOverview Behavior-driven tests of the generator.
 *
 * @author Andy Culbertson
 *
 * @requires NPM:path
 * @requires NPM:yeoman-assert
 * @requires NPM:yeoman-test
 * @requires NPM:fs
 */

'use strict';
const path = require('path');
const assert = require('yeoman-assert');
const helpers = require('yeoman-test');
const fs = require('fs');
const process = require('process');
const childProcess = require('child_process');

/**
 * Return an absolute path converted from the given relative path.
 *
 * @param {String}   relPath An relative path.
 *
 * @returns {String} The absolute path based on the current working directory.
 */
var absPath = function (relPath) {
  return process.cwd() + '/' + relPath;
};

/**
 * Return an Array of absolute paths converted from the given relative paths.
 *
 * @param {Array}   relPaths An Array of relative paths.
 *
 * @returns {Array} The absolute paths based on the current working directory.
 */
var absPaths = function (relPaths) {
  return relPaths.map(function (relPath) {
    return absPath(relPath);
  });
};

// Adapted from https://gist.github.com/kethinov/6658166
/**
 * Return an array of files and directories found within dir, including the contents of its subdirectories.
 *
 * @param   {String}         dir      The relative path of the directory you're listing.
 * @param   {String}         dirBase  The absolute base for the relative path.
 * @param   {Array.<String>} itemList The list of paths to append to. This should usually be empty.
 * @param   {String}         itemType The type of filesystem item to return: 'all', 'dir', 'emptyDir', or 'file'.
 *
 * @returns {Array.<String>} The list of files and directories under dir.
 */
var walkSync = function (dir, dirBase, itemList, itemType) {
  const dirAbsPath = path.join(dirBase, dir);
  const theseItems = fs.readdirSync(dirAbsPath);
  itemList = itemList || [];
  if (theseItems.length === 0 && itemType === 'emptyDir') {
    itemList.push(dir);
  }

  theseItems.forEach(function (item) {
    const itemRelPath = path.join(dir, item);
    if (fs.statSync(path.join(dirAbsPath, item)).isDirectory()) {
      if (itemType === 'all' || itemType === 'dir') {
        itemList.push(itemRelPath);
      }

      itemList = walkSync(itemRelPath, dirBase, itemList, itemType);
    } else if (itemType === 'all' || itemType === 'file') {
      itemList.push(itemRelPath);
    }
  });
  return itemList;
};

/**
 * Replace '\' with '/' in the given list of paths.
 *
 * @param {Array} paths The paths with possible backslashes.
 *
 * @returns {Array} The paths with forward slashes.
 */
var normalizePathSeparators = function (paths) {
  return paths.map(function (path) {
    return path.replace(/\\/g, '/');
  });
};

// Adapted from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#Implementing_basic_set_operations
/**
 * Return the set difference between setA and setB.
 *
 * @param {Set}   setA The set with potentially extra elements.
 * @param {Set}   setB The set to subtract.
 *
 * @returns {Set} The elements in setA that aren't in setB.
 */
var difference = function (setA, setB) {
  var difference = new Set(setA);
  for (const elem of setB) {
    difference.delete(elem);
  }

  return difference;
};

const packageName = 'cookiefactory';

describe('generator-python-cmd:app', () => {
  beforeAll(() => {
    return helpers.run(path.join(__dirname, '../generators/app'))
      .withPrompts({projectName: 'Cookie Factory',
        packageName: packageName,
        projectDesc: 'A project for managing my coding projects',
        author: 'Andy Culbertson',
        authorEmail: 'thinkulum@gmail.com'});
  });

  const expectedFilePaths = [
    '.editorconfig',
    '.gitignore',
    'app.py',
    'LICENSE.md',
    'README.md',
    'setup.py',
    'requirements.txt',
    'requirements-dev.txt',
    packageName + '/cli.py',
    packageName + '/config.py',
    packageName + '/controller.py',
    packageName + '/__init__.py',
    'tests/context.py',
    'tests/test_app.py',
  ];

  const expectedEmptyDirPaths = [
    'config',
    'docs',
  ];

  it('creates expected files', () => {
    assert.file(absPaths(expectedFilePaths));
  });

  it('doesn\'t create unexpected files', () => {
    const actualFileSet = new Set(
      normalizePathSeparators(
        walkSync('', process.cwd(), [], 'file')));
    const expectedFileSet = new Set(expectedFilePaths);
    assert.equal(
      difference(actualFileSet, expectedFileSet).size,
      0);
  });

  it('creates expected empty directories', () => {
    assert.file(absPaths(expectedEmptyDirPaths));
  });

  it('doesn\'t create unexpected empty directories', () => {
    const actualEmptyDirSet = new Set(
      normalizePathSeparators(
        walkSync('', process.cwd(), [], 'emptyDir')));
    const expectedEmptyDirSet = new Set(expectedEmptyDirPaths);
    assert.equal(
      difference(actualEmptyDirSet, expectedEmptyDirSet).size,
      0);
  });

  it('creates expected file content', () => {
    var fileContentItems = [];
    var i;
    for (i = 0; i < expectedFilePaths.length; i++) {
      const expectedFilePath = expectedFilePaths[i];
      const contentFilePath = path.join(
        __dirname,
        'data',
        expectedFilePath.replace(
          /^\./, ''));
      const expectedContent = fs.readFileSync(
        contentFilePath, 'utf8', (err, data) => {
          if (err) {
            throw err;
          }

          return data;
        });

      fileContentItems.push(
        [absPath(expectedFilePath), expectedContent]);
    }

    assert.fileContent(fileContentItems);
  });

  it('passes the app tests', () => {
    const result = childProcess.execSync('pytest');
    const failure = new RegExp('= \\d+ failed');
    assert.ok(failure.test(result) === false);
  });
});
<|endoftext|>


<commit_before>var express = require('express');
var notelyServerApp = express();

notelyServerApp.get('/', function(req, res) {
  res.send('I <3 Express!');
});

notelyServerApp.listen(3030, function() {
  console.log('Listening on http://localhost:3030');
});
<commit_msg>Serve static notes.<commit_after>var express = require('express');
var notelyServerApp = express();

notelyServerApp.get('/', function(req, res) {
  res.json([
    {
      title: 'Hardcoded note',
      body_html: 'Cool note. Aww, shucks.'
    },
    {
      title: 'Another hardcoded note',
      body_html: "Ain't life grand?"
    }
  ]);
});

notelyServerApp.listen(3030, function() {
  console.log('Listening on http://localhost:3030');
});
<|endoftext|>


<commit_before>// # Themes API
// RESTful API for Themes
var Promise            = require('bluebird'),
    _                  = require('lodash'),
    canThis            = require('../permissions').canThis,
    config             = require('../config'),
    errors             = require('../errors'),
    settings           = require('./settings'),
    themes;

/**
 * ## Themes API Methods
 *
 * **See:** [API Methods](index.js.html#api%20methods)
 */
themes = {
    /**
     * ### Browse
     * Get a list of all the available themes
     * @param {{context}} options
     * @returns {Promise(Themes)}
     */
    browse: function browse(options) {
        options = options || {};

        return canThis(options.context).browse.theme().then(function () {
            return Promise.all([
                settings.read({key: 'activeTheme', context: {internal: true}}),
                config.paths.availableThemes
            ]).then(function (result) {
                var activeTheme = result[0].settings[0].value,
                    availableThemes = result[1],
                    themes = [],
                    themeKeys = Object.keys(availableThemes);

                _.each(themeKeys, function (key) {
                    if (key.indexOf('.') !== 0
                            && key !== '_messages'
                            && key !== 'README.md'
                            ) {
                        var item = {
                            uuid: key
                        };

                        if (availableThemes[key].hasOwnProperty('package.json')) {
                            item = _.merge(item, availableThemes[key]['package.json']);
                        }

                        item.active = item.uuid === activeTheme;

                        themes.push(item);
                    }
                });

                return {themes: themes};
            });
        }, function () {
            return Promise.reject(new errors.NoPermissionError('You do not have permission to browse themes.'));
        });
    },

    /**
     * ### Edit
     * Change the active theme
     * @param {Theme} object
     * @param {{context}} options
     * @returns {Promise(Theme)}
     */
    edit: function edit(object, options) {
        var themeName;

        // Check whether the request is properly formatted.
        if (!_.isArray(object.themes)) {
            return Promise.reject({type: 'BadRequest', message: 'Invalid request.'});
        }

        themeName = object.themes[0].uuid;

        return canThis(options.context).edit.theme().then(function () {
            return themes.browse(options).then(function (availableThemes) {
                var theme;

                // Check if the theme exists
                theme = _.find(availableThemes.themes, function (currentTheme) {
                    return currentTheme.uuid === themeName;
                });

                if (!theme) {
                    return Promise.reject(new errors.BadRequestError('Theme does not exist.'));
                }

                // Activate the theme
                return settings.edit(
                    {settings: [{key: 'activeTheme', value: themeName}]}, {context: {internal: true}}
                ).then(function () {
                    theme.active = true;
                    return {themes: [theme]};
                });
            });
        }, function () {
            return Promise.reject(new errors.NoPermissionError('You do not have permission to edit themes.'));
        });
    }
};

module.exports = themes;
<commit_msg>Refactor themes endpoint with pipeline<commit_after>// # Themes API
// RESTful API for Themes
var Promise            = require('bluebird'),
    _                  = require('lodash'),
    config             = require('../config'),
    errors             = require('../errors'),
    settings           = require('./settings'),
    pipeline           = require('../utils/pipeline'),
    utils              = require('./utils'),

    docName = 'themes',
    themes;

/**
 * ### Fetch Active Theme
 * @returns {Theme} theme
 */

function fetchActiveTheme() {
    return settings.read({
        key: 'activeTheme',
        context: {
            internal: true
        }
    }).then(function (response) {
        return response.settings[0].value;
    });
}

/**
 * ### Fetch Available Themes
 * @returns {Themes} themes
 */

function fetchAvailableThemes() {
    var themes = {};

    _.each(config.paths.availableThemes, function (theme, name) {
        var isTheme = name.indexOf('.') !== 0 && name !== '_messages' && name.toLowerCase() !== 'readme.md';

        if (!isTheme) {
            return;
        }

        themes[name] = theme;
    });

    return themes;
}

/**
 * ### Activate Theme
 * @param {Theme} theme
 * @returns {Object} response
 */

function activateTheme(theme) {
    return settings.edit({
        settings: [{
            key: 'activeTheme',
            value: theme.name
        }],
        context: {
            internal: true
        }
    }).then(function () {
        theme.active = true;

        return {themes: [theme]};
    });
}

/**
 * ## Themes API Methods
 *
 * **See:** [API Methods](index.js.html#api%20methods)
 */
themes = {
    /**
     * ### Browse
     * Get a list of all the available themes
     * @param {{context}} options
     * @returns {Promise(Themes)}
     */
    browse: function browse(options) {
        var tasks;

        /**
         * ### Model Query
         * @returns {Object} result
         */

        function modelQuery() {
            var result = {
                availableThemes: fetchAvailableThemes(),
                activeTheme: fetchActiveTheme()
            };

            return Promise.props(result);
        }

        /**
         * ### Build response
         * @param {Object} result - result from modelQuery()
         * @returns {Object} response
         */

        function buildResponse(result) {
            var themes = [];

            _.each(result.availableThemes, function (theme, name) {
                var item = {
                    active: result.activeTheme === name,
                    uuid: name
                };

                // if theme has package.json file,
                // merge its properties
                if (theme['package.json']) {
                    item = _.merge(item, theme['package.json']);
                }

                themes.push(item);
            });

            return {themes: themes};
        }

        tasks = [
            utils.validate(docName),
            utils.handlePublicPermissions(docName, 'browse'),
            modelQuery,
            buildResponse
        ];

        return pipeline(tasks, options || {});
    },

    /**
     * ### Edit
     * Change the active theme
     * @param {Theme} object
     * @param {{context}} options
     * @returns {Promise(Theme)}
     */
    edit: function edit(object, options) {
        var tasks, themeName;

        // Check whether the request is properly formatted.
        if (!_.isArray(object.themes)) {
            return Promise.reject(new errors.BadRequestError('Invalid request.'));
        }

        themeName = object.themes[0].uuid;

        /**
         * ### Model Query
         * @param {Object} options
         * @returns {Theme} theme
         */

        function modelQuery(options) {
            return themes.browse(options).then(function (response) {
                var theme = _.find(response.themes, function (theme) {
                    return theme.uuid === themeName;
                });

                if (!theme) {
                    return Promise.reject(new errors.BadRequestError('Theme does not exist.'));
                }

                if (!theme.name) {
                    theme.name = themeName;
                }

                return theme;
            });
        }

        tasks = [
            utils.validate(docName),
            utils.handlePermissions(docName, 'edit'),
            modelQuery,
            activateTheme
        ];

        return pipeline(tasks, options || {});
    }
};

module.exports = themes;
<|endoftext|>


<commit_before>describe('Sync tickets from Boxoffice', function() {
  const { admin, user } = require('../fixtures/user.js');
  const project = require('../fixtures/project.json');
  const events = require('../fixtures/events.json');
  const { ticket_client } = require('../fixtures/boxoffice.js');

  it('Sync tickets from Boxoffice', function() {
    cy.relogin('/testcypressproject');
    cy.get('[data-cy-project="' + project.title + '"]')
      .first()
      .click();
    cy.location('pathname').should('contain', project.url);
    cy.get('a[data-cy-navbar="settings"]').click();
    cy.location('pathname').should('contain', 'settings');
    cy.get('a[data-cy="checkin"').click();
    cy.location('pathname').should('contain', '/admin');

    cy.get('a[data-cy="new-ticket-client"').click();
    cy.get('#name').type(ticket_client.client_id);
    cy.get('#clientid').type(ticket_client.client_id);
    cy.get('#client_eventid').type(ticket_client.ic_id);
    cy.get('#client_secret').type(ticket_client.secret);
    cy.get('#client_access_token').type(ticket_client.access_key);
    cy.get('button')
      .contains('Add ticket client')
      .click();

    cy.get('button[data-cy="sync-tickets"').click();
    cy.wait(120000);

    cy.fixture('events').then(events => {
      events.forEach(function(event) {
        cy.get('li[data-cy-ticket="' + event.title + '"]')
          .find('a[data-cy="ticket-edit"]')
          .click();
        cy.get('label')
          .contains(event.title)
          .click();
        cy.get('button')
          .contains('Save changes')
          .click();
      });
    });

    cy.get('button[data-cy="sync-tickets"').click();
    cy.wait(120000);

    cy.get('a[data-cy="' + events[0].title + '"]').click();
    cy.get('td[data-cy="participant"]').contains(user.username);
    cy.get('a[data-cy="back-to-setup"]').click();

    cy.get('a[data-cy="' + events[1].title + '"]').click();
    cy.get('td[data-cy="participant"]').contains(user.username);
    cy.get('a[data-cy="back-to-setup"]').click();
  });
});
<commit_msg>Don't log the secret values in cypress test<commit_after>describe('Sync tickets from Boxoffice', function() {
  const { admin, user } = require('../fixtures/user.js');
  const project = require('../fixtures/project.json');
  const events = require('../fixtures/events.json');
  const { ticket_client } = require('../fixtures/boxoffice.js');

  it('Sync tickets from Boxoffice', function() {
    cy.relogin('/testcypressproject');
    cy.get('[data-cy-project="' + project.title + '"]')
      .first()
      .click();
    cy.location('pathname').should('contain', project.url);
    cy.get('a[data-cy-navbar="settings"]').click();
    cy.location('pathname').should('contain', 'settings');
    cy.get('a[data-cy="checkin"').click();
    cy.location('pathname').should('contain', '/admin');

    cy.get('a[data-cy="new-ticket-client"').click();
    cy.get('#name').type(ticket_client.client_id, { log: false });
    cy.get('#clientid').type(ticket_client.client_id, { log: false });
    cy.get('#client_eventid').type(ticket_client.ic_id, { log: false });
    cy.get('#client_secret').type(ticket_client.secret, { log: false });
    cy.get('#client_access_token').type(ticket_client.access_key, {
      log: false,
    });
    cy.get('button')
      .contains('Add ticket client')
      .click();

    cy.get('button[data-cy="sync-tickets"').click();
    cy.wait(120000);

    cy.fixture('events').then(events => {
      events.forEach(function(event) {
        cy.get('li[data-cy-ticket="' + event.title + '"]')
          .find('a[data-cy="ticket-edit"]')
          .click();
        cy.get('label')
          .contains(event.title)
          .click();
        cy.get('button')
          .contains('Save changes')
          .click();
      });
    });

    cy.get('button[data-cy="sync-tickets"').click();
    cy.wait(120000);
    cy.get('button[data-cy="sync-tickets"').click();

    cy.get('a[data-cy="' + events[0].title + '"]').click();
    cy.get('td[data-cy="participant"]').contains(user.username);
    cy.get('a[data-cy="back-to-setup"]').click();

    cy.get('a[data-cy="' + events[1].title + '"]').click();
    cy.get('td[data-cy="participant"]').contains(user.username);
    cy.get('a[data-cy="back-to-setup"]').click();
  });
});
<|endoftext|>


<commit_before>class ShopListService {
    constructor() {

    }

    extractAndSortProducts(list) {
        return this.sortProducts(this.extractProducts(list));
    }

    extractProducts(list) {
        const extracted = [];
        list.map(ingestion => ingestion.list.map(meal => meal.dishesList.map(food => {
            if (!food.hasIngredients) {
                extracted.push(food)
            } else if (food.hasIngredients) {
                food.list.map(product => extracted.push(product));
            }
        })));
        extracted.forEach(a => {
            a.name = a.name.trim();
        });
        return extracted;
    }

    sortProducts(list) {
        const sorted = [];
        list.forEach((a) => {
            let thisNameIndex = sorted.findIndex(b => b.name === a.name);
            if (thisNameIndex > -1) {
                sorted[thisNameIndex].quantity++;
            } else {
                sorted.push(angular.copy(a));
            }
        });
        return sorted;
    }
}
Mealpler.service('ShopListService', ShopListService);<commit_msg>MP-34: fixed shoplist extracting products<commit_after>class ShopListService {
    constructor() {

    }

    extractAndSortProducts(list) {
        return this.sortProducts(this.extractProducts(list));
    }

    extractProducts(list) {
        const extracted = [];
        list.map(ingestion => ingestion.mealsList.map(meal => {
            if (meal.dishesList.length > 0) {
                meal.dishesList.map(food => {
                    if (!food.hasIngredients) {
                        extracted.push(food)
                    } else if (food.hasIngredients) {
                        food.list.map(product => extracted.push(product));
                    }
                })
            }
        }));
        extracted.forEach(a => {
            a.name = a.name.trim();
        });
        return extracted;
    }

    sortProducts(list) {
        const sorted = [];
        list.forEach((a) => {
            let thisNameIndex = sorted.findIndex(b => b.name === a.name);
            if (thisNameIndex > -1) {
                sorted[thisNameIndex].quantity++;
            } else {
                sorted.push(angular.copy(a));
            }
        });
        return sorted;
    }
}
Mealpler.service('ShopListService', ShopListService);<|endoftext|>


<commit_before>$ = jQuery;

var mainImage;
var currentMainImageIndex;
var previousImages = new Array();
var nextImages = new Array();
var futureImages = new Array();
var imageSourceCache = new Array();
var stepsToMove;
var imageIndex = 0;

/**
	Executed after page was loaded.
*/
$(document).ready(function () {
	initializeImages();
	initializeCache();
});

/**
	Adds the object references to the different arrays and variables.
*/
function initializeImages() {
	mainImage = $('#mainImage');
	previousImages[0] = $('#previousImage01');
	previousImages[1] = $('#previousImage02');
	previousImages[2] = $('#previousImage03');
	previousImages[3] = $('#previousImage04');
	nextImages[0] = $('#image01');
	nextImages[1] = $('#image02');
	nextImages[2] = $('#image03');
	nextImages[3] = $('#image04');
	futureImages[0] = $('#futureImage01');
	futureImages[1] = $('#futureImage02');
	futureImages[2] = $('#futureImage03');
	futureImages[3] = $('#futureImage04');
}

function initLightBox() {
	var galleryLightboxSettings = {
		captionSelector : ".caption",
		captionAttr : false,
		captionHTML : true,
	};
	$('a.lightbox').lightBox(); // Select all links with lightbox class
}

/**
	Loads the image paths into the cache.
*/
function initializeCache() {
	$.ajax({
		url: "vote/getdata/json",
		success: function(result){
			for (var i = 0; i < result.length; i++) {
				imageSourceCache[i] = {
					imageId: result[i].imageId,
					imageThumbPath: result[i].imageThumbPath,
					imagePath: result[i].imagePath,
					images: result[i].images,
					imagesCount: result[i].images.images.length
				};
			}
			// load the first image sources from the cache
			mainImage.attr('src', imageSourceCache[imageIndex].imagePath);

			// write the current number of images to the diashow link
			$('#numberOfImages').html(imageSourceCache[imageIndex].imagesCount);

			currentMainImageIndex = imageIndex;
			prepareSlideShow();

			initLightBox();

			var autoHeight = mainImage.attr('height');
			$("#mainImageFieldset").animate({ height: autoHeight + 20 });

			imageIndex = 1;
			loadImagesFromCache();
		},
		error: function(result){
			alert("error");
		}
	});

	/*for (var i = 0; i < 14; i++) {
		imageSourceCache[i] = { imagePath: '/sites/all/modules/vote/img/test/image' + (i + 1) + '.jpg' };
	}*/
}

/**
	Loads the images from the cache using the imageIndex variable as index.
*/
function loadImagesFromCache() {
	for (var i = 0; i < nextImages.length; i ++) {

		// load previous thumbnail and fullsize images into cache
		var cleanIndex = checkIndex(imageSourceCache, imageIndex - nextImages.length + i);
		previousImages[i].attr('src', imageSourceCache[cleanIndex].imageThumbPath);
		$('#mainImageContainer').append("");

		// load next thumbnail and fullsize images into cache
		nextImages[i].attr('src', imageSourceCache[imageIndex + i].imageThumbPath);
		$('#mainImageContainer').append("");

		// load future thumbnail and fullsize images into cache
		cleanIndex = checkIndex(imageSourceCache, imageIndex + nextImages.length + i);
		futureImages[i].attr('src', imageSourceCache[cleanIndex].imageThumbPath);
		$('#mainImageContainer').append("");

		previousImages[i].css({ width: 0, opacity: 0 });

		if (i < futureImages.length) {
			futureImages[i].css({ width: 0, opacity: 0 });
		}

		nextImages[i].css({ width: 190, opacity: 1 });
	}
}

/**
	Switches to the next image.
*/
function nextImage() {
	moveImages(1, true);
}

/**
	Checks if currently there are animations in progress and returns true if so.
*/
function finishedAllAnimations() {
	return $(".nextImages:animated, #mainImage:animated, #mainImageFieldset:animated, #mainImageContainer:animated").length == 0;
}

/**
 * Prepares the images for the lightbox pop-up slideshow.
 */
function prepareSlideShow() {
	// change the ID of the current image onclick handler
	$('#mainImageLink').attr("href", imageSourceCache[currentMainImageIndex].imagePath);

	// load slideshow images for current main image
	$('#slideshowImages').html('');
	for (var j = 1; j < imageSourceCache[currentMainImageIndex].imagesCount; j ++) {
		var currentPath = '/gallery/observation/' + imageSourceCache[currentMainImageIndex].imageId + '/thumb/' + imageSourceCache[currentMainImageIndex].images.images[j].id + '/fullsize';
		$('#slideshowImages').append("");
	}
}

/**
	Move the image index using the given number of steps (e.g. -4 moves four images back, 3 moves three images forward).
	If the boolan replaceMainImage is set to true, the main image will be replaced with the destination image.
*/
function moveImages(steps, replaceMainImage) {
	if (!finishedAllAnimations()) {
		return;
	}

	stepsToMove = steps;

	animateMainImage(replaceMainImage);
}

/**
	Animates the main image replacement.
*/
function animateMainImage(replaceMainImage) {
	currentMainImageIndex = imageIndex + stepsToMove - 1;

	if (replaceMainImage) {
		// Fade main image out
		$("#mainImageContainer").animate({
				opacity: 0
			}, 500, function() {
				/**
					Animation complete of main image fade out
					Move the selected image to the main image position
				*/
				nextImages[stepsToMove - 1].animate({
						left: "-=" + (nextImages[stepsToMove - 1].position().left - mainImage.position().left),
						top: "-=" + (nextImages[stepsToMove - 1].position().top - mainImage.position().top)
					}, 500, function() {
						/**
							Animation complete of selected image movement
							Switch the main image source with the selected image source
						*/
						mainImage.attr("src", imageSourceCache[currentMainImageIndex].imagePath);

						// write the current number of images to the diashow link
						$('#numberOfImages').html(imageSourceCache[currentMainImageIndex].imagesCount);

						prepareSlideShow();

						initLightBox();

						// Calculate the new height of the image
						mainImage.css({ width: 400 });
						var autoHeight = mainImage.attr('height');
						// Show the main image and animate it to the normal size
						mainImage.css({ width: 190, height: "auto" });
						$("#mainImageContainer").css({ opacity: 1 });

						mainImage.animate({ width: 400 });
						$("#mainImageFieldset").animate({ height: autoHeight + 20 });
						// Reset position of the selected image
						nextImages[stepsToMove - 1].removeAttr("style");
						// check if the amount of steps is possible to do
						animateNextImages(replaceMainImage);
					}
				);
			}
		);
	} else {
		// check if the amount of steps is possible to do
		animateNextImages(replaceMainImage);
	}
}

/**
	Animates the next images.
*/
function animateNextImages(replaceMainImage) {
	var tempStepsToMove = stepsToMove;

	// if we step below the beginning of the image cache, reduce the steps to move so we exactly hit the beginning
	if (imageIndex + stepsToMove < 0) {
		stepsToMove = -imageIndex;
	}

	// if we step over the end of the image cache, reduce the steps to move so we exactly hit the ending
	if (imageIndex + stepsToMove + nextImages.length >= imageSourceCache.length) {
		stepsToMove = imageSourceCache.length - nextImages.length - imageIndex;
	}

	// set the new image index to reload the images
	imageIndex += stepsToMove;

	var positive = stepsToMove >= 0;
	for (var i = 0; i < Math.abs(stepsToMove); i ++) {
		if (positive) {
			if (i < futureImages.length) {
				futureImages[i].animate({ width: 190, opacity: 1 }, 500);
			}
			if (i < nextImages.length) {
				nextImages[i].animate({ width: 0, opacity: 0 }, 500, function() {
					loadImagesFromCache();
				});
			}
		} else {
			if (i < previousImages.length) {
				var cleanIndex = checkIndex(previousImages, nextImages.length - i - 1);
				previousImages[cleanIndex].animate({ width: 190, opacity: 1 }, 500);
			}
			if (i < nextImages.length) {
				nextImages[nextImages.length - i - 1].animate({ width: 0, opacity: 0 }, 500, function() {
					loadImagesFromCache();
				});
			}
		}
	}
}

/**
	Checks if the index is below zero or above the array length and returns a valid index
*/
function checkIndex(array, index) {
	return index < 0 ? 0 : index >= array.length ? array.length - 1 : index;
}<commit_msg>implemented endless image loop<commit_after>$ = jQuery;

var mainImage;
var currentMainImageIndex;
var previousImages = new Array();
var nextImages = new Array();
var futureImages = new Array();
var imageSourceCache = new Array();
var stepsToMove;
var imageIndex = 0;

/**
	Executed after page was loaded.
*/
$(document).ready(function () {
	initializeImages();
	initializeCache();
});

/**
	Adds the object references to the different arrays and variables.
*/
function initializeImages() {
	mainImage = $('#mainImage');
	previousImages[0] = $('#previousImage01');
	previousImages[1] = $('#previousImage02');
	previousImages[2] = $('#previousImage03');
	previousImages[3] = $('#previousImage04');
	nextImages[0] = $('#image01');
	nextImages[1] = $('#image02');
	nextImages[2] = $('#image03');
	nextImages[3] = $('#image04');
	futureImages[0] = $('#futureImage01');
	futureImages[1] = $('#futureImage02');
	futureImages[2] = $('#futureImage03');
	futureImages[3] = $('#futureImage04');
}

/**
	Loads the image paths into the cache.
*/
function initializeCache() {
	$.ajax({
		url: "vote/getdata/json",
		success: function(result){
			for (var i = 0; i < result.length; i++) {
				imageSourceCache[i] = {
					imageId: result[i].imageId,
					imageThumbPath: result[i].imageThumbPath,
					imagePath: result[i].imagePath,
					images: result[i].images,
					imagesCount: result[i].images.images.length
				};
			}
			// load the first image sources from the cache
			mainImage.attr('src', imageSourceCache[imageIndex].imagePath);

			// write the current number of images to the diashow link
			$('#numberOfImages').html(imageSourceCache[imageIndex].imagesCount);

			currentMainImageIndex = imageIndex;
			prepareSlideShow();

			initLightBox();

			var autoHeight = mainImage.attr('height');
			$("#mainImageFieldset").animate({ height: autoHeight + 20 });

			imageIndex = 1;
			loadImagesFromCache();
		},
		error: function(result){
			alert("error");
		}
	});
}

function initLightBox() {
	var galleryLightboxSettings = {
		captionSelector : ".caption",
		captionAttr : false,
		captionHTML : true,
	};
	$('a.lightbox').lightBox(); // Select all links with lightbox class
}

/**
	Loads the images from the cache using the imageIndex variable as index.
*/
function loadImagesFromCache() {
	for (var i = 0; i < nextImages.length; i ++) {

		// load previous thumbnail and fullsize images into cache
		var cleanIndex = checkIndex(imageIndex - nextImages.length + i);
		previousImages[i].attr('src', imageSourceCache[cleanIndex].imageThumbPath);
		$('#mainImageContainer').append("");

		// load next thumbnail and fullsize images into cache
		cleanIndex = checkIndex(imageIndex + i);
		nextImages[i].attr('src', imageSourceCache[cleanIndex].imageThumbPath);
		$('#mainImageContainer').append("");

		// load future thumbnail and fullsize images into cache
		cleanIndex = checkIndex(imageIndex + nextImages.length + i);
		futureImages[i].attr('src', imageSourceCache[cleanIndex].imageThumbPath);
		$('#mainImageContainer').append("");

		previousImages[i].css({ width: 0, opacity: 0 });

		if (i < futureImages.length) {
			futureImages[i].css({ width: 0, opacity: 0 });
		}

		nextImages[i].css({ width: 190, opacity: 1 });
	}
}

/**
	Switches to the next image.
*/
function nextImage() {
	moveImages(1, true);
}

/**
	Checks if currently there are animations in progress and returns true if so.
*/
function finishedAllAnimations() {
	return $(".nextImages:animated, #mainImage:animated, #mainImageFieldset:animated, #mainImageContainer:animated").length == 0;
}

/**
 * Prepares the images for the lightbox pop-up slideshow.
 */
function prepareSlideShow() {
	// change the ID of the current image onclick handler
	$('#mainImageLink').attr("href", imageSourceCache[currentMainImageIndex].imagePath);

	// load slideshow images for current main image
	$('#slideshowImages').html('');
	for (var j = 1; j < imageSourceCache[currentMainImageIndex].imagesCount; j ++) {
		var currentPath = '/gallery/observation/' + imageSourceCache[currentMainImageIndex].imageId + '/thumb/' + imageSourceCache[currentMainImageIndex].images.images[j].id + '/fullsize';
		$('#slideshowImages').append("");
	}
}

/**
	Move the image index using the given number of steps (e.g. -4 moves four images back, 3 moves three images forward).
	If the boolan replaceMainImage is set to true, the main image will be replaced with the destination image.
*/
function moveImages(steps, replaceMainImage) {
	if (!finishedAllAnimations()) {
		return;
	}

	stepsToMove = steps;

	animateMainImage(replaceMainImage);
}

/**
	Animates the main image replacement.
*/
function animateMainImage(replaceMainImage) {
	currentMainImageIndex = checkIndex(imageIndex + stepsToMove - 1);

	if (replaceMainImage) {
		// Fade main image out
		$("#mainImageContainer").animate({
				opacity: 0
			}, 500, function() {
				/**
					Animation complete of main image fade out
					Move the selected image to the main image position
				*/
				nextImages[stepsToMove - 1].animate({
						left: "-=" + (nextImages[stepsToMove - 1].position().left - mainImage.position().left),
						top: "-=" + (nextImages[stepsToMove - 1].position().top - mainImage.position().top)
					}, 500, function() {
						/**
							Animation complete of selected image movement
							Switch the main image source with the selected image source
						*/
						mainImage.attr("src", imageSourceCache[currentMainImageIndex].imagePath);

						// write the current number of images to the diashow link
						$('#numberOfImages').html(imageSourceCache[currentMainImageIndex].imagesCount);

						prepareSlideShow();

						initLightBox();

						// Calculate the new height of the image
						mainImage.css({ width: 400 });
						var autoHeight = mainImage.attr('height');
						// Show the main image and animate it to the normal size
						mainImage.css({ width: 190, height: "auto" });
						$("#mainImageContainer").css({ opacity: 1 });

						mainImage.animate({ width: 400 });
						$("#mainImageFieldset").animate({ height: autoHeight + 20 });
						// Reset position of the selected image
						nextImages[stepsToMove - 1].removeAttr("style");
						// check if the amount of steps is possible to do
						animateNextImages(replaceMainImage);
					}
				);
			}
		);
	} else {
		// check if the amount of steps is possible to do
		animateNextImages(replaceMainImage);
	}
}

/**
	Animates the next images.
*/
function animateNextImages(replaceMainImage) {
	var tempStepsToMove = stepsToMove;

	// if we step below the beginning of the image cache, reduce the steps to move so we exactly hit the beginning
	/*if (imageIndex + stepsToMove < 0) {
		stepsToMove = -imageIndex;
	}*/

	// if we step over the end of the image cache, reduce the steps to move so we exactly hit the ending
	/*if (imageIndex + stepsToMove + nextImages.length >= imageSourceCache.length) {
		stepsToMove = imageSourceCache.length - nextImages.length - imageIndex;
	}*/

	// set the new image index to reload the images
	imageIndex = checkIndex(stepsToMove + imageIndex);

	/*if(imageIndex < 0) {
		imageIndex -= imageSourceCache.length;
		console.log(imageIndex);
	}*/

	var positive = stepsToMove >= 0;
	for (var i = 0; i < Math.abs(stepsToMove); i ++) {
		if (positive) {
			if (i < futureImages.length) {
				futureImages[i].animate({ width: 190, opacity: 1 }, 500);
			}
			if (i < nextImages.length) {
				nextImages[i].animate({ width: 0, opacity: 0 }, 500, function() {
					loadImagesFromCache();
				});
			}
		} else {
			if (i < previousImages.length) {
				var cleanIndex = nextImages.length - i - 1;
				if (cleanIndex < 0) {
					cleanIndex = 0;
				}
				previousImages[cleanIndex].animate({ width: 190, opacity: 1 }, 500);
			}
			if (i < nextImages.length) {
				nextImages[nextImages.length - i - 1].animate({ width: 0, opacity: 0 }, 500, function() {
					loadImagesFromCache();
				});
			}
		}
	}
}

/**
	Checks if the index is below zero or above the array length and returns a valid index (regarding the image loop)
*/
function checkIndex(index) {
	if (index < 0) {
		return imageSourceCache.length + index;
	} else if (index >= imageSourceCache.length) {
		return index % imageSourceCache.length;
	} else {
		return index;
	}
}<|endoftext|>


<commit_before>import { without } from 'substance'

import {
  AbstractAPI, DynamicCollection,
  StringModel, TextModel, FlowContentModel
} from '../kit'

import ContribsModel from './models/ContribsModel'
import MetaModel from './models/MetaModel'
import renderEntity from './shared/renderEntity'
import TranslateableModel from './models/TranslateableModel'
import TranslationModel from './models/TranslationModel'

import { REQUIRED_PROPERTIES } from './ArticleConstants'

// TODO: this should come from configuration
const COLLECTIONS = {
  'author': 'authors',
  'editor': 'editors'
}

export default class ArticleAPI extends AbstractAPI {
  constructor (articleSession, modelRegistry) {
    super()

    this.modelRegistry = modelRegistry
    this.articleSession = articleSession
    this.article = articleSession.getDocument()
  }

  /*
    Get corresponding model for a given node. This used for most block content types (e.g. Figure, Heading etc.)
  */
  getModel (type, node) {
    let ModelClass = this.modelRegistry[type]
    // HACK: trying to retrieve the node if it is not given
    if (!node) {
      node = this.article.get(type)
    }
    if (ModelClass) {
      return new ModelClass(this, node)
    } else if (node) {
      return this._getModelForNode(node)
    }
  }

  /*
    Returns an entity model (not node!)
  */
  getEntity(entityId) {
    let entityNode = this.article.get(entityId)
    let model = this.getModel(entityNode.type, entityNode)
    return model
  }

  _getNode(nodeId) {
    return this.article.get(nodeId)
  }

  getEntitiesByType(type) {
    let entityIds = this.article.findByType(type)
    return entityIds.map(entityId => this.getEntity(entityId))
  }

  // TODO: this should be configurable. As it is similar to HTML conversion
  // we could use the converter registry for this
  renderEntity(model) {
    let entity = this.getArticle().get(model.id)
    return renderEntity(entity)
  }

  getArticle () {
    return this.article
  }

  getArticleSession () {
    return this.articleSession
  }

  /*
    Returns an entity model (not node!)
  */
  addEntity(data, type) {
    const newNode = Object.assign({}, data, {
      type: type
    })
    let node
    this.articleSession.transaction(tx => {
      tx.selection = null
      node = tx.create(newNode)
    })
    return this.getModel(node.type, node)
  }

  /*
    Returns an entity model (not node!)
  */
  deleteEntity(entityId) {
    let node
    this.articleSession.transaction((tx) => {
      node = tx.delete(entityId)
      tx.selection = null
    })
    return this.getModel(node.type, node)
  }

  addAuthor(person = {}) {
    this._addPerson(person, 'authors')
  }

  addEditor(person = {}) {
    this._addPerson(person, 'editors')
  }

  _addPerson(person = {}, type) {
    const newNode = Object.assign({}, person, {
      type: 'person'
    })
    let node
    this.articleSession.transaction(tx => {
      node = tx.create(newNode)
      const articleRecord = tx.get('article-record')
      let length = articleRecord[type].length
      tx.update(['article-record', type], { type: 'insert', pos: length, value: node.id })
      tx.selection = null
    })
    return this.getModel(node.type, node)
  }

  getAuthors() {
    return this._getPersons('authors')
  }

  getAuthorsModel() {
    return this.getModel('authors')
  }

  getEditors() {
    return this._getPersons('editors')
  }

  _getPersons(prop) {
    let articleRecord = this._getNode('article-record')
    let persons = articleRecord[prop].map(personId => this.getEntity(personId))
    return persons
  }

  deleteAuthor(personId) {
    return this._deletePerson(personId, 'authors')
  }

  deleteEditor(personId) {
    return this._deletePerson(personId, 'editors')
  }

  _deletePerson(personId, type) {
    let node
    this.articleSession.transaction((tx) => {
      node = tx.delete(personId)
      const articleRecord = tx.get('article-record')
      let pos = articleRecord[type].indexOf(personId)
      if (pos !== -1) {
        tx.update(['article-record', type], { type: 'delete', pos: pos })
      }
      tx.selection = null
    })
    return this.getModel(node.type, node)
  }

  addOrganisation(organisation = {}) {
    const orgModel = this.addEntity(organisation, 'organisation')
    return orgModel
  }

  deleteOrganisation(orgId) {
    const model = this.deleteEntity(orgId)
    return model
  }

  addAward(award = {}) {
    const awardModel = this.addEntity(award, 'award')
    return awardModel
  }

  deleteAward(awardId) {
    const model = this.deleteEntity(awardId)
    return model
  }

  addKeyword(keyword = {}) {
    const keywordModel = this.addEntity(keyword, 'keyword')
    return keywordModel
  }

  deleteKeyword(keywordId) {
    const model = this.deleteEntity(keywordId)
    return model
  }

  addSubject(subject = {}) {
    const subjectModel = this.addEntity(subject, 'subject')
    return subjectModel
  }

  deleteSubject(subjectId) {
    const model = this.deleteEntity(subjectId)
    return model
  }

  addReference(reference = {}, type) {
    const articleSession = this.articleSession
    const referenceModel = this.addEntity(reference, type)
    articleSession.transaction(tx => {
      const refEl = tx.createElement('ref').attr('rid',referenceModel.id)
      const refList = tx.find('ref-list')
      refList.append(refEl)
      tx.selection = null
    })
    return referenceModel
  }

  addReferences(refs) {
    const refContribProps = ['authors','editors','inventors','sponsors','translators']
    const articleSession = this.articleSession
    articleSession.transaction(tx => {
      const refList = tx.find('ref-list')
      refs.forEach(ref => {
        refContribProps.forEach(propName => {
          if(ref[propName]) {
            let refContribs = ref[propName].map(contrib => {
              contrib.type = 'ref-contrib'
              const node = tx.create(contrib)
              return node.id
            })
            ref[propName] = refContribs
          }
        })
        const node = tx.create(ref)
        const refEl = tx.createElement('ref').attr('rid',node.id)
        refList.append(refEl)
      })
      tx.selection = null
    })
    return
  }

  deleteReference(refId) {
    const article = this.getArticle()
    const articleSession = this.articleSession
    const xrefIndex = article.getIndex('xrefs')
    const xrefs = xrefIndex.get(refId)
    articleSession.transaction(tx => {
      const refList = tx.find('ref-list')
      let node = tx.get(refId)
      // ATTENTION: it is important to nodes from the transaction tx!
      // Be careful with closures here.
      refList.removeChild(node)
      tx.delete(node.id)
      // Now update xref targets
      xrefs.forEach(xrefId => {
        let xref = tx.get(xrefId)
        let idrefs = xref.attr('rid').split(' ')
        idrefs = without(idrefs, refId)
        xref.setAttribute('rid', idrefs.join(' '))
      })
      tx.selection = null
    })
  }

  /*
    @return {StringModel} Model for the language code of article's main language
  */
  getOriginalLanguageCode() {
    let article = this.getArticle().getRootNode()
    return new StringModel(this, [article.id, 'attributes', 'xml:lang'])
  }

  getCollectionForType (type) {
    // TODO: need to rethink this.
    // ATM we are registering special model classes for collections,
    // which are named after the single entity they contain,
    // e.g. 'authors' is a the collection of author nodes (which are person nodes essentially).
    // This is currently pretty confusing. It would be better to have
    // specific node in the model for that.
    // Still, we would need a mechanism to specify which node is acting as the parent
    // collection for which node.
    let collectionType = COLLECTIONS[type]
    // I don't like this implicit mapping.
    if (!collectionType) {
      collectionType = type + 's'
    }
    let model = this.getModel(collectionType)
    if (!model) {
      console.error(`No collection specified for type '${type}'. Using DynamicCollection. You should register a collection for this type explicitly.`)
      model = new DynamicCollection(this, type)
    }
    return model
  }

  getSchema(type) {
    return this.article.getSchema().getNodeSchema(type)
  }

  getArticleTitle () {
    let titleNode = this.getArticle().find('article-title')
    return new TextModel(this, titleNode.getPath())
  }

  getArticleAbstract () {
    let abstract = this.getArticle().find('abstract')
    return new FlowContentModel(this, abstract.getContentPath())
  }

  getArticleBody () {
    let body = this.getArticle().find('body')
    return new FlowContentModel(this, body.getContentPath())
  }

  getContribs () {
    let articleMeta = this.getArticle().find('article-meta')
    return new ContribsModel(articleMeta, this._getContext())
  }

  getMeta () {
    let articleMeta = this.getArticle().find('article-meta')
    return new MetaModel(articleMeta, this._getContext())
  }

  getFootnotes () {
    let fns = this.getArticle().findAll('fn-group > fn')
    return fns.map(fn => this.getModel(fn.type, fn))
  }

  getFigures () {
    let figs = this.getArticle().findAll('fig')
    return figs.map(fig => this.getModel(fig.type, fig))
  }

  getTranslatables () {
    return [
      this._getTitleTranslateable(),
      this._getAbstractTranslateable()
    ]
  }

  addTranslation (translatableId, languageCode) {
    if (translatableId === 'title-trans') {
      this._addTitleTranslation(languageCode)
    } else if (translatableId === 'abstract-trans') {
      this._addAbstractTranslation(languageCode)
    }
  }

  _addTitleTranslation(languageCode) {
    const articleSession = this.articleSession
    articleSession.transaction(tx => {
      const titleEl = tx.createElement('trans-title-group').attr('xml:lang', languageCode).append(
        tx.createElement('trans-title')
      )
      const titleGroup = tx.find('title-group')
      titleGroup.append(titleEl)
    })
  }

  _addAbstractTranslation(languageCode) {
    const articleSession = this.articleSession
    articleSession.transaction(tx => {
      const abstractEl = tx.createElement('trans-abstract').attr('xml:lang', languageCode).append(
        tx.createElement('p')
      )
      // TODO: replace it with schema driven smartness
      const abstract = tx.find('article-meta > abstract')
      const articleMeta = abstract.getParent()
      const abtractPos = articleMeta.getChildPosition(abstract)
      articleMeta.insertAt(abtractPos+1, abstractEl)
    })
  }

  deleteTranslation (translatableId, languageCode) {
    if (translatableId === 'title-trans') {
      this._deleteTitleTranslation(languageCode)
    } else if (translatableId === 'abstract-trans') {
      this._deleteAbstractTranslation(languageCode)
    }
  }

  _deleteTitleTranslation(languageCode) {
    const articleSession = this.articleSession
    articleSession.transaction(tx => {
      // HACK: attribute selector with colon is invalid
      const titles = tx.findAll('trans-title-group')
      const titleEl = titles.find(t => t.attr('xml:lang') === languageCode)
      titleEl.parentNode.removeChild(titleEl)
      tx.delete(titleEl.id)
    })
  }

  _deleteAbstractTranslation(languageCode) {
    const articleSession = this.articleSession
    articleSession.transaction(tx => {
      // HACK: attribute selector with colon is invalid
      const abstracts = tx.findAll('trans-abstract')
      const abstractEl = abstracts.find(a => a.attr('xml:lang') === languageCode)
      abstractEl.parentNode.removeChild(abstractEl)
      tx.delete(abstractEl.id)
    })
  }

  _getTitleTranslateable() {
    let transTitleGroups = this.getArticle().findAll('trans-title-group')
    let translatableId = 'title-trans'
    let orinialLanguageCode = this.getOriginalLanguageCode()
    let articleTitle = this.getArticleTitle()
    let translations = transTitleGroups.map(transTitleGroup => {
      let transTitle = transTitleGroup.find('trans-title')
      let textModel = new TextModel(this, transTitle.getPath())
      let languageModel = new StringModel(this, [transTitleGroup.id, 'attributes', 'xml:lang'])
      return new TranslationModel(this, translatableId, textModel, languageModel)
    })
    return new TranslateableModel(
      this,
      translatableId,
      orinialLanguageCode,
      articleTitle,
      translations
    )
  }

  _getAbstractTranslateable() {
    let transAbstracts = this.getArticle().findAll('trans-abstract')
    let translatableId = 'abstract-trans'
    let orinialLanguageCode = this.getOriginalLanguageCode()
    let articleAbstract = this.getArticleAbstract()
    let translations = transAbstracts.map(transAbstract => {
      let transAbstractModel = new FlowContentModel(this, [transAbstract.id, '_children'])
      let languageModel = new StringModel(this, [transAbstract.id, 'attributes', 'xml:lang'])
      return new TranslationModel(this, translatableId, transAbstractModel, languageModel)
    })

    return new TranslateableModel(
      this,
      translatableId,
      orinialLanguageCode,
      articleAbstract,
      translations
    )
  }

  _getContext () {
    return this._context
  }

  /*
    TODO: In the future it should be necessary to expose those managers, instead
    API's should be used to access information.
  */
  getFigureManager () {
    return this.getArticleSession().getFigureManager()
  }

  getFootnoteManager () {
    return this.getArticleSession().getFootnoteManager()
  }

  getReferenceManager () {
    return this.getArticleSession().getReferenceManager()
  }

  getTableManager () {
    return this.getArticleSession().getTableManager()
  }

  get doc () {
    console.error('DEPRECATED: use api.getArticle() instead.')
    return this.getArticle()
  }

  _getDocument () {
    return this.getArticleSession().getDocument()
  }

  _getDocumentSession () {
    return this.getArticleSession()
  }

  _isPropertyRequired (type, propertyName) {
    let REQUIRED = REQUIRED_PROPERTIES[type]
    if (REQUIRED) return REQUIRED.has(propertyName)
    return false
  }
}
<commit_msg>Cleanup.<commit_after>import { without } from 'substance'

import {
  AbstractAPI, DynamicCollection,
  StringModel, TextModel, FlowContentModel
} from '../kit'

import ContribsModel from './models/ContribsModel'
import MetaModel from './models/MetaModel'
import renderEntity from './shared/renderEntity'
import TranslateableModel from './models/TranslateableModel'
import TranslationModel from './models/TranslationModel'

import { REQUIRED_PROPERTIES } from './ArticleConstants'

// TODO: this should come from configuration
const COLLECTIONS = {
  'author': 'authors',
  'editor': 'editors'
}

export default class ArticleAPI extends AbstractAPI {
  constructor (articleSession, modelRegistry) {
    super()

    this.modelRegistry = modelRegistry
    this.articleSession = articleSession
    this.article = articleSession.getDocument()
  }

  /*
    Get corresponding model for a given node. This used for most block content types (e.g. Figure, Heading etc.)
  */
  getModel (type, node) {
    let ModelClass = this.modelRegistry[type]
    // HACK: trying to retrieve the node if it is not given
    if (!node) {
      node = this.article.get(type)
    }
    if (ModelClass) {
      return new ModelClass(this, node)
    } else if (node) {
      return this._getModelForNode(node)
    }
  }

  _getNode(nodeId) {
    return this.article.get(nodeId)
  }

  getEntitiesByType(type) {
    let entityIds = this.article.findByType(type)
    return entityIds.map(entityId => this._getModelById(entityId))
  }

  // TODO: this should be configurable. As it is similar to HTML conversion
  // we could use the converter registry for this
  renderEntity(model) {
    let entity = this.getArticle().get(model.id)
    return renderEntity(entity)
  }

  getArticle () {
    return this.article
  }

  getArticleSession () {
    return this.articleSession
  }

  addAuthor(person = {}) {
    this._addPerson(person, 'authors')
  }

  addEditor(person = {}) {
    this._addPerson(person, 'editors')
  }

  _addPerson(person = {}, type) {
    const newNode = Object.assign({}, person, {
      type: 'person'
    })
    let node
    this.articleSession.transaction(tx => {
      node = tx.create(newNode)
      const articleRecord = tx.get('article-record')
      let length = articleRecord[type].length
      tx.update(['article-record', type], { type: 'insert', pos: length, value: node.id })
      tx.selection = null
    })
    return this.getModel(node.type, node)
  }

  getAuthors() {
    return this._getPersons('authors')
  }

  getAuthorsModel() {
    return this.getModel('authors')
  }

  getEditors() {
    return this._getPersons('editors')
  }

  _getPersons(prop) {
    let articleRecord = this._getNode('article-record')
    let persons = articleRecord[prop].map(personId => this._getModelById(personId))
    return persons
  }

  deleteAuthor(personId) {
    return this._deletePerson(personId, 'authors')
  }

  deleteEditor(personId) {
    return this._deletePerson(personId, 'editors')
  }

  _deletePerson(personId, type) {
    let node
    this.articleSession.transaction((tx) => {
      node = tx.delete(personId)
      const articleRecord = tx.get('article-record')
      let pos = articleRecord[type].indexOf(personId)
      if (pos !== -1) {
        tx.update(['article-record', type], { type: 'delete', pos: pos })
      }
      tx.selection = null
    })
    return this.getModel(node.type, node)
  }

  addReferences(refs) {
    const refContribProps = ['authors','editors','inventors','sponsors','translators']
    const articleSession = this.articleSession
    articleSession.transaction(tx => {
      const refList = tx.find('ref-list')
      refs.forEach(ref => {
        refContribProps.forEach(propName => {
          if(ref[propName]) {
            let refContribs = ref[propName].map(contrib => {
              contrib.type = 'ref-contrib'
              const node = tx.create(contrib)
              return node.id
            })
            ref[propName] = refContribs
          }
        })
        const node = tx.create(ref)
        const refEl = tx.createElement('ref').attr('rid',node.id)
        refList.append(refEl)
      })
      tx.selection = null
    })
    return
  }

  deleteReference(refId) {
    const article = this.getArticle()
    const articleSession = this.articleSession
    const xrefIndex = article.getIndex('xrefs')
    const xrefs = xrefIndex.get(refId)
    articleSession.transaction(tx => {
      const refList = tx.find('ref-list')
      let node = tx.get(refId)
      // ATTENTION: it is important to nodes from the transaction tx!
      // Be careful with closures here.
      refList.removeChild(node)
      tx.delete(node.id)
      // Now update xref targets
      xrefs.forEach(xrefId => {
        let xref = tx.get(xrefId)
        let idrefs = xref.attr('rid').split(' ')
        idrefs = without(idrefs, refId)
        xref.setAttribute('rid', idrefs.join(' '))
      })
      tx.selection = null
    })
  }

  /*
    @return {StringModel} Model for the language code of article's main language
  */
  getOriginalLanguageCode() {
    let article = this.getArticle().getRootNode()
    return new StringModel(this, [article.id, 'attributes', 'xml:lang'])
  }

  getCollectionForType (type) {
    // TODO: need to rethink this.
    // ATM we are registering special model classes for collections,
    // which are named after the single entity they contain,
    // e.g. 'authors' is a the collection of author nodes (which are person nodes essentially).
    // This is currently pretty confusing. It would be better to have
    // specific node in the model for that.
    // Still, we would need a mechanism to specify which node is acting as the parent
    // collection for which node.
    let collectionType = COLLECTIONS[type]
    // I don't like this implicit mapping.
    if (!collectionType) {
      collectionType = type + 's'
    }
    let model = this.getModel(collectionType)
    if (!model) {
      console.error(`No collection specified for type '${type}'. Using DynamicCollection. You should register a collection for this type explicitly.`)
      model = new DynamicCollection(this, type)
    }
    return model
  }

  getSchema(type) {
    return this.article.getSchema().getNodeSchema(type)
  }

  getArticleTitle () {
    let titleNode = this.getArticle().find('article-title')
    return new TextModel(this, titleNode.getPath())
  }

  getArticleAbstract () {
    let abstract = this.getArticle().find('abstract')
    return new FlowContentModel(this, abstract.getContentPath())
  }

  getArticleBody () {
    let body = this.getArticle().find('body')
    return new FlowContentModel(this, body.getContentPath())
  }

  getContribs () {
    let articleMeta = this.getArticle().find('article-meta')
    return new ContribsModel(articleMeta, this._getContext())
  }

  getMeta () {
    let articleMeta = this.getArticle().find('article-meta')
    return new MetaModel(articleMeta, this._getContext())
  }

  getFootnotes () {
    let fns = this.getArticle().findAll('fn-group > fn')
    return fns.map(fn => this.getModel(fn.type, fn))
  }

  getFigures () {
    let figs = this.getArticle().findAll('fig')
    return figs.map(fig => this.getModel(fig.type, fig))
  }

  getTranslatables () {
    return [
      this._getTitleTranslateable(),
      this._getAbstractTranslateable()
    ]
  }

  addTranslation (translatableId, languageCode) {
    if (translatableId === 'title-trans') {
      this._addTitleTranslation(languageCode)
    } else if (translatableId === 'abstract-trans') {
      this._addAbstractTranslation(languageCode)
    }
  }

  _addTitleTranslation(languageCode) {
    const articleSession = this.articleSession
    articleSession.transaction(tx => {
      const titleEl = tx.createElement('trans-title-group').attr('xml:lang', languageCode).append(
        tx.createElement('trans-title')
      )
      const titleGroup = tx.find('title-group')
      titleGroup.append(titleEl)
    })
  }

  _addAbstractTranslation(languageCode) {
    const articleSession = this.articleSession
    articleSession.transaction(tx => {
      const abstractEl = tx.createElement('trans-abstract').attr('xml:lang', languageCode).append(
        tx.createElement('p')
      )
      // TODO: replace it with schema driven smartness
      const abstract = tx.find('article-meta > abstract')
      const articleMeta = abstract.getParent()
      const abtractPos = articleMeta.getChildPosition(abstract)
      articleMeta.insertAt(abtractPos+1, abstractEl)
    })
  }

  deleteTranslation (translatableId, languageCode) {
    if (translatableId === 'title-trans') {
      this._deleteTitleTranslation(languageCode)
    } else if (translatableId === 'abstract-trans') {
      this._deleteAbstractTranslation(languageCode)
    }
  }

  _deleteTitleTranslation(languageCode) {
    const articleSession = this.articleSession
    articleSession.transaction(tx => {
      // HACK: attribute selector with colon is invalid
      const titles = tx.findAll('trans-title-group')
      const titleEl = titles.find(t => t.attr('xml:lang') === languageCode)
      titleEl.parentNode.removeChild(titleEl)
      tx.delete(titleEl.id)
    })
  }

  _deleteAbstractTranslation(languageCode) {
    const articleSession = this.articleSession
    articleSession.transaction(tx => {
      // HACK: attribute selector with colon is invalid
      const abstracts = tx.findAll('trans-abstract')
      const abstractEl = abstracts.find(a => a.attr('xml:lang') === languageCode)
      abstractEl.parentNode.removeChild(abstractEl)
      tx.delete(abstractEl.id)
    })
  }

  _getTitleTranslateable() {
    let transTitleGroups = this.getArticle().findAll('trans-title-group')
    let translatableId = 'title-trans'
    let orinialLanguageCode = this.getOriginalLanguageCode()
    let articleTitle = this.getArticleTitle()
    let translations = transTitleGroups.map(transTitleGroup => {
      let transTitle = transTitleGroup.find('trans-title')
      let textModel = new TextModel(this, transTitle.getPath())
      let languageModel = new StringModel(this, [transTitleGroup.id, 'attributes', 'xml:lang'])
      return new TranslationModel(this, translatableId, textModel, languageModel)
    })
    return new TranslateableModel(
      this,
      translatableId,
      orinialLanguageCode,
      articleTitle,
      translations
    )
  }

  _getAbstractTranslateable() {
    let transAbstracts = this.getArticle().findAll('trans-abstract')
    let translatableId = 'abstract-trans'
    let orinialLanguageCode = this.getOriginalLanguageCode()
    let articleAbstract = this.getArticleAbstract()
    let translations = transAbstracts.map(transAbstract => {
      let transAbstractModel = new FlowContentModel(this, [transAbstract.id, '_children'])
      let languageModel = new StringModel(this, [transAbstract.id, 'attributes', 'xml:lang'])
      return new TranslationModel(this, translatableId, transAbstractModel, languageModel)
    })

    return new TranslateableModel(
      this,
      translatableId,
      orinialLanguageCode,
      articleAbstract,
      translations
    )
  }

  _getContext () {
    return this._context
  }

  /*
    TODO: In the future it should be necessary to expose those managers, instead
    API's should be used to access information.
  */
  getFigureManager () {
    return this.getArticleSession().getFigureManager()
  }

  getFootnoteManager () {
    return this.getArticleSession().getFootnoteManager()
  }

  getReferenceManager () {
    return this.getArticleSession().getReferenceManager()
  }

  getTableManager () {
    return this.getArticleSession().getTableManager()
  }

  get doc () {
    console.error('DEPRECATED: use api.getArticle() instead.')
    return this.getArticle()
  }

  _getDocument () {
    return this.getArticleSession().getDocument()
  }

  _getDocumentSession () {
    return this.getArticleSession()
  }

  _isPropertyRequired (type, propertyName) {
    let REQUIRED = REQUIRED_PROPERTIES[type]
    if (REQUIRED) return REQUIRED.has(propertyName)
    return false
  }
}
<|endoftext|>


<commit_before><commit_msg>Properly order boolean checks.<commit_after><|endoftext|>


<commit_before>import styled, { css } from "styled-components";
import { colors } from "@crave/farmblocks-theme";
import { CENTER } from "../constants/alignments";

const Container = styled.div`position: relative;`;

const centerAlignment = css`
  left: 50%;
  transform: translateX(-50%);
`;

const alignX = coordinate => ({ align }) => {
  if (align === CENTER) {
    return centerAlignment;
  }

  return css`${align}: ${coordinate}`;
};

const StyledTooltip = styled.div`
  visibility: ${props => (props.isVisible ? "visible" : "hidden")};
  position: absolute;
  z-index: ${props => props.zIndex};
  top: 15px;
  padding: 8px;
  background-color: #ffffff;
  box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16);
  border: solid 1px rgba(0, 0, 0, 0.16);
  border-radius: 4px;
  white-space: pre;
  color: ${colors.CARBON};

  font-family: lato, sans-serif;

  ${alignX(0)};

  &:after,
  &:before {
    bottom: 100%;
    border: solid 1px rgba(0, 0, 0, 0.16);
    content: " ";
    height: 0;
    width: 0;
    position: absolute;
    pointer-events: none;
  }

  &:before {
    border-color: transparent;
    border-bottom-color: rgba(0, 0, 0, 0.16);
    border-width: 8px;
    ${alignX("7px")};
  }

  &:after {
    border-color: transparent;
    border-bottom-color: #ffffff;
    border-width: 7px;
    ${alignX("8px")};
  }
`;

export { Container, StyledTooltip };
<commit_msg>feat(Tooltip): add animation<commit_after>import styled, { css } from "styled-components";
import { colors } from "@crave/farmblocks-theme";
import { CENTER } from "../constants/alignments";

const Container = styled.div`position: relative;`;

const centerAlignment = css`
  left: 50%;
  transform: translateX(-50%);
`;

const alignX = coordinate => ({ align }) => {
  if (align === CENTER) {
    return centerAlignment;
  }

  return css`
    ${align}: ${coordinate};
  `;
};

const visibility = ({ isVisible }) => {
  if (isVisible) {
    return css`
      visibility: visible;
      opacity: 1;
      transition-delay: 0s;
    `;
  }

  return css`
    visibility: hidden;
    opacity: 0;
  `;
};

const StyledTooltip = styled.div`
  transition: visibility 0s linear 0.1s, opacity 0.1s linear;
  ${visibility};

  position: absolute;
  z-index: ${props => props.zIndex};
  top: 15px;
  padding: 8px;
  background-color: #ffffff;
  box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16);
  border: solid 1px rgba(0, 0, 0, 0.16);
  border-radius: 4px;
  white-space: pre;
  color: ${colors.CARBON};

  font-family: lato, sans-serif;

  ${alignX(0)};

  &:after,
  &:before {
    bottom: 100%;
    border: solid 1px rgba(0, 0, 0, 0.16);
    content: " ";
    height: 0;
    width: 0;
    position: absolute;
    pointer-events: none;
  }

  &:before {
    border-color: transparent;
    border-bottom-color: rgba(0, 0, 0, 0.16);
    border-width: 8px;
    ${alignX("7px")};
  }

  &:after {
    border-color: transparent;
    border-bottom-color: #ffffff;
    border-width: 7px;
    ${alignX("8px")};
  }
`;

export { Container, StyledTooltip };
<|endoftext|>


<commit_before>/* global describe, it, before, after */
const mockery = require('mockery')
const sinon = require('sinon')
const Immutable = require('immutable')
const process = require('process')
const assert = require('assert')
const uuid = require('uuid')
const path = require('path')
const fakeElectron = require('../../../lib/fakeElectron')

const appConstants = require('../../../../../js/constants/appConstants')
const {PENDING, IN_PROGRESS, RESUMING, PAUSED, COMPLETED, CANCELLED, INTERRUPTED} = require('../../../../../js/constants/downloadStates')
const {CANCEL} = require('../../../../../app/common/constants/electronDownloadItemActions')
require('../../../braveUnit')

const downloadId = (state, i = 0) => Object.keys(state.get('downloads').toJS())[i]
const downloadUrl = 'http://www.bradrichter.com/mostHatedPrimes.txt'
const savePath = path.join(require('os').tmpdir(), 'mostHatedPrimes.txt')
const oneDownloadWithState = (state) => Immutable.fromJS({
  downloads: {
    [uuid.v4()]: {
      startTime: new Date().getTime(),
      filename: 'mostHatedPrimes.txt',
      savePath,
      url: downloadUrl,
      totalBytes: 104729,
      receivedBytes: 96931,
      state
    }
  }
})

describe('downloadsReducer', function () {
  let downloadsReducer
  before(function () {
    mockery.enable({
      warnOnReplace: false,
      warnOnUnregistered: false,
      useCleanCache: true
    })
    mockery.registerMock('electron', fakeElectron)
    downloadsReducer = require('../../../../../app/browser/reducers/downloadsReducer')
  })

  after(function () {
    mockery.disable()
  })

  it('returns original state for unhandled actions', function () {
    const oldState = oneDownloadWithState(IN_PROGRESS)
    const newState = downloadsReducer(oldState, {actionType: uuid.v4()})
    assert.deepEqual(newState.toJS(), oldState.toJS())
  })

  describe('APP_DOWNLOAD_REVEALED', function () {
    it('Reveals file for paths that does not exist exist', function (cb) {
      sinon.stub(fakeElectron.shell, 'showItemInFolder', (path) => {
        fakeElectron.shell.showItemInFolder.restore()
        assert.equal(path, process.cwd())
        cb()
      })
      let oldState = oneDownloadWithState(IN_PROGRESS)
      oldState = oldState.setIn(['downloads', downloadId(oldState), 'savePath'], process.cwd())
      downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_REVEALED, downloadId: downloadId(oldState)})
    })
    it('Reveals file for paths that does not exist exist', function (cb) {
      const saveDir = path.dirname(savePath)
      sinon.stub(fakeElectron.shell, 'openItem', (path) => {
        fakeElectron.shell.openItem.restore()
        assert.equal(path, saveDir)
        cb()
      })
      const oldState = oneDownloadWithState(IN_PROGRESS)
      downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_REVEALED, downloadId: downloadId(oldState)})
    })
  })

  describe('APP_DOWNLOAD_OPENED', function () {
    it('Opens a downloaded file', function (cb) {
      sinon.stub(fakeElectron.shell, 'openItem', (path) => {
        fakeElectron.shell.openItem.restore()
        assert.equal(path, process.cwd())
        cb()
      })
      let oldState = oneDownloadWithState(IN_PROGRESS)
      oldState = oldState.setIn(['downloads', downloadId(oldState), 'savePath'], process.cwd())
      downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_OPENED, downloadId: downloadId(oldState)})
    })
    it('Beeps when a downloaded file is trying to be opened', function (cb) {
      sinon.stub(fakeElectron.shell, 'beep', () => {
        fakeElectron.shell.beep.restore()
        cb()
      })
      const oldState = oneDownloadWithState(IN_PROGRESS)
      downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_OPENED, downloadId: downloadId(oldState)})
    })
  })

  describe('APP_DOWNLOAD_ACTION_PERFORMED', function () {
    it('CANCEL causes CANCELLED state', function () {
      const oldState = oneDownloadWithState(IN_PROGRESS)
      const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_ACTION_PERFORMED, downloadId: downloadId(oldState), downloadAction: CANCEL})
      assert.equal(newState.getIn(['downloads', downloadId(oldState), 'state']), CANCELLED)
    })
  })

  describe('APP_DOWNLOAD_COPIED_TO_CLIPBOARD', function () {
    it('copies the download URL to the clipboard', function () {
      const spy = sinon.spy(fakeElectron.clipboard, 'writeText')
      const oldState = oneDownloadWithState(IN_PROGRESS)
      downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_COPIED_TO_CLIPBOARD, downloadId: downloadId(oldState)})
      assert(spy.withArgs(downloadUrl).calledOnce)
      fakeElectron.clipboard.writeText.restore()
    })
  })

  describe('APP_DOWNLOAD_DELETED', function () {
    it('deletes a downloadId that exists', function (cb) {
      let oldState = oneDownloadWithState(IN_PROGRESS)
      const existingPath = process.cwd()
      oldState = oldState.setIn(['downloads', downloadId(oldState), 'savePath'], existingPath)
      sinon.stub(fakeElectron.shell, 'moveItemToTrash', (path) => {
        assert.equal(path, existingPath)
        fakeElectron.shell.moveItemToTrash.restore()
        cb()
      })
      const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_DELETED, downloadId: downloadId(oldState)})
      assert.equal(Object.keys(newState.get('downloads').toJS()).length, 0)
    })
    it('does nothing for a downloadId that does not exist', function () {
      const oldState = oneDownloadWithState(IN_PROGRESS)
      const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_DELETED, downloadId: uuid.v4()})
      assert.deepEqual(newState.toJS(), oldState.toJS())
    })
  })

  describe('APP_DOWNLOAD_CLEARED', function () {
    it('clears download item', function () {
      const oldState = oneDownloadWithState(IN_PROGRESS)
      const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_DELETED, downloadId: downloadId(oldState)})
      assert.equal(Object.keys(newState.get('downloads').toJS()).length, 0)
    })
    it('does nothing for a downloadId that does not exist', function () {
      const oldState = oneDownloadWithState(IN_PROGRESS)
      const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_DELETED, downloadId: uuid.v4()})
      assert.deepEqual(newState.toJS(), oldState.toJS())
    })
  })

  describe('APP_DOWNLOAD_REDOWNLOADED', function () {
    it('should redownload the same URL', function (cb) {
      const win = {
        webContents: {
          downloadURL: function () {
          }
        }
      }
      sinon.stub(win.webContents, 'downloadURL', (redownloadUrl) => {
        assert.equal(redownloadUrl, downloadUrl)
        cb()
      })
      sinon.stub(fakeElectron.BrowserWindow, 'getFocusedWindow', (path) => {
        return win
      })
      const oldState = oneDownloadWithState(CANCELLED)
      downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_REDOWNLOADED, downloadId: downloadId(oldState)})
    })
  })

  describe('APP_MERGE_DOWNLOAD_DETAIL', function () {
    it('should update downloads', function () {
      const oldState = oneDownloadWithState(PENDING)
      const newState = downloadsReducer(oldState,
        {
          actionType: appConstants.APP_MERGE_DOWNLOAD_DETAIL,
          downloadId: downloadId(oldState),
          downloadDetail: {state: COMPLETED}
        }
      )
      assert.equal(newState.getIn(['downloads', downloadId(oldState), 'state']), COMPLETED)
    })
    it('should not update for invalid download Ids', function () {
      const oldState = oneDownloadWithState(PENDING)
      const newState = downloadsReducer(oldState,
        {
          actionType: appConstants.APP_MERGE_DOWNLOAD_DETAIL,
          downloadId: uuid.v4(),
          downloadDetail: {state: COMPLETED}
        }
      )
      assert.equal(newState.getIn(['downloads', downloadId(oldState), 'state']), PENDING)
    })
    it('should add new download IDs as needed', function () {
      const oldState = oneDownloadWithState(PENDING)
      const downloadId = uuid.v4()
      const newState = downloadsReducer(oldState,
        {
          actionType: appConstants.APP_MERGE_DOWNLOAD_DETAIL,
          downloadId,
          downloadDetail: {state: COMPLETED}
        }
      )
      assert.equal(newState.getIn(['downloads', downloadId, 'state']), COMPLETED)
      assert.equal(Object.keys(newState.get('downloads').toJS()).length, 2)
    })
  })

  describe('APP_CLEAR_COMPLETED_DOWNLOADS', function () {
    it('should clear completed downloads', function () {
      const states = [COMPLETED, CANCELLED, INTERRUPTED]
      states.forEach((state) => {
        const newState = downloadsReducer(oneDownloadWithState(state), {actionType: appConstants.APP_CLEAR_COMPLETED_DOWNLOADS})
        assert.equal(Object.keys(newState.get('downloads').toJS()).length, 0)
      })
    })
    it('should not clear downloads when they are still in progress', function () {
      const states = [PENDING, IN_PROGRESS, RESUMING, PAUSED]
      states.forEach((state) => {
        const newState = downloadsReducer(oneDownloadWithState(state), {actionType: appConstants.APP_CLEAR_COMPLETED_DOWNLOADS})
        assert.equal(Object.keys(newState.get('downloads').toJS()).length, 1)
      })
    })
  })

  describe('APP_DOWNLOAD_DEFAULT_PATH', function () {
    describe('when showing folder selection dialog', function () {
      let stub
      let options

      before(function () {
        stub = sinon.stub(fakeElectron.dialog, 'showOpenDialog', function (arg1, arg2) {
          options = arg2
        })
        downloadsReducer({}, {actionType: appConstants.APP_DOWNLOAD_DEFAULT_PATH})
      })

      after(function () {
        stub.restore()
      })

      it('calls dialog.showOpenDialog', function () {
        assert(stub.calledOnce)
      })

      it('passes the correct defaultPath', function () {
        assert.equal(options.defaultPath, `${process.cwd()}/downloads`)
      })

      it('passes the correct properties object', function () {
        assert.deepEqual(options.properties, ['openDirectory'])
      })
    })
  })
})
<commit_msg>Fixes failing unit test that was introduced with #10481<commit_after>/* global describe, it, before, after */
const mockery = require('mockery')
const sinon = require('sinon')
const Immutable = require('immutable')
const process = require('process')
const assert = require('assert')
const uuid = require('uuid')
const path = require('path')
const fakeElectron = require('../../../lib/fakeElectron')

const appConstants = require('../../../../../js/constants/appConstants')
const {PENDING, IN_PROGRESS, RESUMING, PAUSED, COMPLETED, CANCELLED, INTERRUPTED} = require('../../../../../js/constants/downloadStates')
const {CANCEL} = require('../../../../../app/common/constants/electronDownloadItemActions')
require('../../../braveUnit')

const downloadId = (state, i = 0) => Object.keys(state.get('downloads').toJS())[i]
const downloadUrl = 'http://www.bradrichter.com/mostHatedPrimes.txt'
const savePath = path.join(require('os').tmpdir(), 'mostHatedPrimes.txt')
const oneDownloadWithState = (state) => Immutable.fromJS({
  downloads: {
    [uuid.v4()]: {
      startTime: new Date().getTime(),
      filename: 'mostHatedPrimes.txt',
      savePath,
      url: downloadUrl,
      totalBytes: 104729,
      receivedBytes: 96931,
      state
    }
  }
})

describe('downloadsReducer', function () {
  let downloadsReducer
  before(function () {
    mockery.enable({
      warnOnReplace: false,
      warnOnUnregistered: false,
      useCleanCache: true
    })
    mockery.registerMock('electron', fakeElectron)
    downloadsReducer = require('../../../../../app/browser/reducers/downloadsReducer')
  })

  after(function () {
    mockery.disable()
  })

  it('returns original state for unhandled actions', function () {
    const oldState = oneDownloadWithState(IN_PROGRESS)
    const newState = downloadsReducer(oldState, {actionType: uuid.v4()})
    assert.deepEqual(newState.toJS(), oldState.toJS())
  })

  describe('APP_DOWNLOAD_REVEALED', function () {
    it('Reveals file for paths that does not exist exist', function (cb) {
      sinon.stub(fakeElectron.shell, 'showItemInFolder', (path) => {
        fakeElectron.shell.showItemInFolder.restore()
        assert.equal(path, process.cwd())
        cb()
      })
      let oldState = oneDownloadWithState(IN_PROGRESS)
      oldState = oldState.setIn(['downloads', downloadId(oldState), 'savePath'], process.cwd())
      downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_REVEALED, downloadId: downloadId(oldState)})
    })
    it('Reveals file for paths that does not exist exist', function (cb) {
      const saveDir = path.dirname(savePath)
      sinon.stub(fakeElectron.shell, 'openItem', (path) => {
        fakeElectron.shell.openItem.restore()
        assert.equal(path, saveDir)
        cb()
      })
      const oldState = oneDownloadWithState(IN_PROGRESS)
      downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_REVEALED, downloadId: downloadId(oldState)})
    })
  })

  describe('APP_DOWNLOAD_OPENED', function () {
    it('Opens a downloaded file', function (cb) {
      sinon.stub(fakeElectron.shell, 'openItem', (path) => {
        fakeElectron.shell.openItem.restore()
        assert.equal(path, process.cwd())
        cb()
      })
      let oldState = oneDownloadWithState(IN_PROGRESS)
      oldState = oldState.setIn(['downloads', downloadId(oldState), 'savePath'], process.cwd())
      downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_OPENED, downloadId: downloadId(oldState)})
    })
    it('Beeps when a downloaded file is trying to be opened', function (cb) {
      sinon.stub(fakeElectron.shell, 'beep', () => {
        fakeElectron.shell.beep.restore()
        cb()
      })
      const oldState = oneDownloadWithState(IN_PROGRESS)
      downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_OPENED, downloadId: downloadId(oldState)})
    })
  })

  describe('APP_DOWNLOAD_ACTION_PERFORMED', function () {
    it('CANCEL causes CANCELLED state', function () {
      const oldState = oneDownloadWithState(IN_PROGRESS)
      const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_ACTION_PERFORMED, downloadId: downloadId(oldState), downloadAction: CANCEL})
      assert.equal(newState.getIn(['downloads', downloadId(oldState), 'state']), CANCELLED)
    })
  })

  describe('APP_DOWNLOAD_COPIED_TO_CLIPBOARD', function () {
    it('copies the download URL to the clipboard', function () {
      const spy = sinon.spy(fakeElectron.clipboard, 'writeText')
      const oldState = oneDownloadWithState(IN_PROGRESS)
      downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_COPIED_TO_CLIPBOARD, downloadId: downloadId(oldState)})
      assert(spy.withArgs(downloadUrl).calledOnce)
      fakeElectron.clipboard.writeText.restore()
    })
  })

  describe('APP_DOWNLOAD_DELETED', function () {
    it('deletes a downloadId that exists', function (cb) {
      let oldState = oneDownloadWithState(IN_PROGRESS)
      const existingPath = process.cwd()
      oldState = oldState.setIn(['downloads', downloadId(oldState), 'savePath'], existingPath)
      sinon.stub(fakeElectron.shell, 'moveItemToTrash', (path) => {
        assert.equal(path, existingPath)
        fakeElectron.shell.moveItemToTrash.restore()
        cb()
      })
      const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_DELETED, downloadId: downloadId(oldState)})
      assert.equal(Object.keys(newState.get('downloads').toJS()).length, 0)
    })
    it('does nothing for a downloadId that does not exist', function () {
      const oldState = oneDownloadWithState(IN_PROGRESS)
      const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_DELETED, downloadId: uuid.v4()})
      assert.deepEqual(newState.toJS(), oldState.toJS())
    })
  })

  describe('APP_DOWNLOAD_CLEARED', function () {
    it('clears download item', function () {
      const oldState = oneDownloadWithState(IN_PROGRESS)
      const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_DELETED, downloadId: downloadId(oldState)})
      assert.equal(Object.keys(newState.get('downloads').toJS()).length, 0)
    })
    it('does nothing for a downloadId that does not exist', function () {
      const oldState = oneDownloadWithState(IN_PROGRESS)
      const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_DELETED, downloadId: uuid.v4()})
      assert.deepEqual(newState.toJS(), oldState.toJS())
    })
  })

  describe('APP_DOWNLOAD_REDOWNLOADED', function () {
    it('should redownload the same URL', function (cb) {
      const win = {
        webContents: {
          downloadURL: function () {
          }
        }
      }
      sinon.stub(win.webContents, 'downloadURL', (redownloadUrl) => {
        assert.equal(redownloadUrl, downloadUrl)
        cb()
      })
      sinon.stub(fakeElectron.BrowserWindow, 'getFocusedWindow', (path) => {
        return win
      })
      const oldState = oneDownloadWithState(CANCELLED)
      downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_REDOWNLOADED, downloadId: downloadId(oldState)})
    })
  })

  describe('APP_MERGE_DOWNLOAD_DETAIL', function () {
    it('should update downloads', function () {
      const oldState = oneDownloadWithState(PENDING)
      const newState = downloadsReducer(oldState,
        {
          actionType: appConstants.APP_MERGE_DOWNLOAD_DETAIL,
          downloadId: downloadId(oldState),
          downloadDetail: {state: COMPLETED}
        }
      )
      assert.equal(newState.getIn(['downloads', downloadId(oldState), 'state']), COMPLETED)
    })
    it('should not update for invalid download Ids', function () {
      const oldState = oneDownloadWithState(PENDING)
      const newState = downloadsReducer(oldState,
        {
          actionType: appConstants.APP_MERGE_DOWNLOAD_DETAIL,
          downloadId: uuid.v4(),
          downloadDetail: {state: COMPLETED}
        }
      )
      assert.equal(newState.getIn(['downloads', downloadId(oldState), 'state']), PENDING)
    })
    it('should add new download IDs as needed', function () {
      const oldState = oneDownloadWithState(PENDING)
      const downloadId = uuid.v4()
      const newState = downloadsReducer(oldState,
        {
          actionType: appConstants.APP_MERGE_DOWNLOAD_DETAIL,
          downloadId,
          downloadDetail: {state: COMPLETED}
        }
      )
      assert.equal(newState.getIn(['downloads', downloadId, 'state']), COMPLETED)
      assert.equal(Object.keys(newState.get('downloads').toJS()).length, 2)
    })
  })

  describe('APP_CLEAR_COMPLETED_DOWNLOADS', function () {
    it('should clear completed downloads', function () {
      const states = [COMPLETED, CANCELLED, INTERRUPTED]
      states.forEach((state) => {
        const newState = downloadsReducer(oneDownloadWithState(state), {actionType: appConstants.APP_CLEAR_COMPLETED_DOWNLOADS})
        assert.equal(Object.keys(newState.get('downloads').toJS()).length, 0)
      })
    })
    it('should not clear downloads when they are still in progress', function () {
      const states = [PENDING, IN_PROGRESS, RESUMING, PAUSED]
      states.forEach((state) => {
        const newState = downloadsReducer(oneDownloadWithState(state), {actionType: appConstants.APP_CLEAR_COMPLETED_DOWNLOADS})
        assert.equal(Object.keys(newState.get('downloads').toJS()).length, 1)
      })
    })
  })

  describe('APP_DOWNLOAD_DEFAULT_PATH', function () {
    describe('when showing folder selection dialog', function () {
      let stub
      let options

      before(function () {
        stub = sinon.stub(fakeElectron.dialog, 'showOpenDialog', function (arg1, arg2) {
          options = arg2
        })
        downloadsReducer({}, {actionType: appConstants.APP_DOWNLOAD_DEFAULT_PATH})
      })

      after(function () {
        stub.restore()
      })

      it('calls dialog.showOpenDialog', function () {
        assert(stub.calledOnce)
      })

      it('passes the correct defaultPath', function () {
        assert.equal(options.defaultPath, `${process.cwd()}/downloads`)
      })

      it('passes the correct properties object', function () {
        assert.deepEqual(options.properties, ['openDirectory', 'createDirectory'])
      })
    })
  })
})
<|endoftext|>


<commit_before>import { useState } from 'react';
import gql from 'graphql-tag';
import getConfig from 'next/config';
import { t } from 'ttag';
import { useQuery } from '@apollo/react-hooks';
import { Box, Chip } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import AddIcon from '@material-ui/icons/Add';

import ArticleCategory from './ArticleCategory';
import AddCategoryDialog from './AddCategoryDialog';

const {
  publicRuntimeConfig: { PUBLIC_SHOW_ADD_CATEGORY },
} = getConfig();

const useStyles = makeStyles(theme => ({
  root: {
    marginTop: -theme.spacing(1), // Counter margin-top for article category
  },
  button: {
    marginTop: theme.spacing(1),
  },
  buttonIcon: {
    fontSize: theme.typography.body1.fontSize,
    marginRight: 4,
  },
}));

const CATEGORY_LIST_QUERY = gql`
  query ListCategoryForSelect {
    ListCategories(first: 50) {
      edges {
        node {
          id
          ...CategoryData
        }
      }
    }
  }
  ${AddCategoryDialog.fragments.CategoryData}
`;

function ArticleCategories({
  articleId,
  articleCategories,
  similarCategories,
}) {
  const classes = useStyles();
  const [showAddDialog, setAddDialogShow] = useState(true);
  const { data, loading } = useQuery(CATEGORY_LIST_QUERY, {
    ssr: false,
  });

  const isInArticle = (articleCategories || []).reduce(
    (map, { categoryId }) => {
      map[categoryId] = true;
      return map;
    },
    {}
  );

  //sort by similar category on top
  const allCategories = (data?.ListCategories?.edges || [])
    .map(({ node }) => node)
    .sort((x, y) => {
      return similarCategories.includes(x.id) ===
        similarCategories.includes(y.id)
        ? 0
        : similarCategories.includes(x.id)
        ? -1
        : 1;
    });

  const hasOtherCategories = allCategories.some(({ id }) => !isInArticle[id]);

  return (
    <aside className={classes.root}>
      {(articleCategories || []).map(articleCategory => (
        <ArticleCategory
          key={articleCategory.categoryId}
          {...articleCategory}
        />
      ))}

      {PUBLIC_SHOW_ADD_CATEGORY && !loading && hasOtherCategories && (
        <Chip
          className={classes.button}
          onClick={() => setAddDialogShow(true)}
          variant="outlined"
          label={
            <Box display="flex" alignItems="center" pr={1}>
              <AddIcon className={classes.buttonIcon} />
              {t`Suggest categories`}
            </Box>
          }
        />
      )}

      {showAddDialog && (
        <AddCategoryDialog
          articleId={articleId}
          articleCategories={articleCategories}
          allCategories={allCategories}
          onClose={() => {
            setAddDialogShow(false);
          }}
        />
      )}
    </aside>
  );
}

ArticleCategories.fragments = {
  ...ArticleCategory.fragments,
  ...AddCategoryDialog.fragments,
};

export default ArticleCategories;
<commit_msg>revert back to false state<commit_after>import { useState } from 'react';
import gql from 'graphql-tag';
import getConfig from 'next/config';
import { t } from 'ttag';
import { useQuery } from '@apollo/react-hooks';
import { Box, Chip } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import AddIcon from '@material-ui/icons/Add';

import ArticleCategory from './ArticleCategory';
import AddCategoryDialog from './AddCategoryDialog';

const {
  publicRuntimeConfig: { PUBLIC_SHOW_ADD_CATEGORY },
} = getConfig();

const useStyles = makeStyles(theme => ({
  root: {
    marginTop: -theme.spacing(1), // Counter margin-top for article category
  },
  button: {
    marginTop: theme.spacing(1),
  },
  buttonIcon: {
    fontSize: theme.typography.body1.fontSize,
    marginRight: 4,
  },
}));

const CATEGORY_LIST_QUERY = gql`
  query ListCategoryForSelect {
    ListCategories(first: 50) {
      edges {
        node {
          id
          ...CategoryData
        }
      }
    }
  }
  ${AddCategoryDialog.fragments.CategoryData}
`;

function ArticleCategories({
  articleId,
  articleCategories,
  similarCategories,
}) {
  const classes = useStyles();
  const [showAddDialog, setAddDialogShow] = useState(false);
  const { data, loading } = useQuery(CATEGORY_LIST_QUERY, {
    ssr: false,
  });

  const isInArticle = (articleCategories || []).reduce(
    (map, { categoryId }) => {
      map[categoryId] = true;
      return map;
    },
    {}
  );

  //sort by similar category on top
  const allCategories = (data?.ListCategories?.edges || [])
    .map(({ node }) => node)
    .sort((x, y) => {
      return similarCategories.includes(x.id) ===
        similarCategories.includes(y.id)
        ? 0
        : similarCategories.includes(x.id)
        ? -1
        : 1;
    });

  const hasOtherCategories = allCategories.some(({ id }) => !isInArticle[id]);

  return (
    <aside className={classes.root}>
      {(articleCategories || []).map(articleCategory => (
        <ArticleCategory
          key={articleCategory.categoryId}
          {...articleCategory}
        />
      ))}

      {PUBLIC_SHOW_ADD_CATEGORY && !loading && hasOtherCategories && (
        <Chip
          className={classes.button}
          onClick={() => setAddDialogShow(true)}
          variant="outlined"
          label={
            <Box display="flex" alignItems="center" pr={1}>
              <AddIcon className={classes.buttonIcon} />
              {t`Suggest categories`}
            </Box>
          }
        />
      )}

      {showAddDialog && (
        <AddCategoryDialog
          articleId={articleId}
          articleCategories={articleCategories}
          allCategories={allCategories}
          onClose={() => {
            setAddDialogShow(false);
          }}
        />
      )}
    </aside>
  );
}

ArticleCategories.fragments = {
  ...ArticleCategory.fragments,
  ...AddCategoryDialog.fragments,
};

export default ArticleCategories;
<|endoftext|>