<commit_before>import Ember from 'ember';
import config from 'ember-get-config';

import { validator, buildValidations } from 'ember-cp-validations';

import permissions from 'ember-osf/const/permissions';
import NodeActionsMixin from 'ember-osf/mixins/node-actions';
import TaggableMixin from 'ember-osf/mixins/taggable-mixin';

import loadAll from 'ember-osf/utils/load-relationship';

// Enum of available upload states
export const State = Object.freeze(Ember.Object.create({
    START: 'start',
    NEW: 'new',
    EXISTING: 'existing'
}));

/*****************************
  Form data and validations
 *****************************/
const BasicsValidations = buildValidations({
    basicsAbstract: {
        description: 'Abstract',
        validators: [
            validator('presence', true),
            validator('length', {
                // currently min of 20 characters -- this is what arXiv has as the minimum length of an abstract
                min: 20,
                max: 5000
            })
        ]
    },
    basicsDOI: {
        description: 'DOI',
        validators: [
            validator('format', {
                // Simplest regex- try not to diverge too much from the backend
                regex: /^10\.\S+\//,
                allowBlank: true,
                message: 'Please use a valid {description}'
            })
        ]
    }
});

/**
 * "Add preprint" page definitions
 */
export default Ember.Controller.extend(BasicsValidations, NodeActionsMixin, TaggableMixin, {
    _State: State,
    filePickerState: State.START,
    fileManager: Ember.inject.service(),
    toast: Ember.inject.service('toast'),
    panelActions: Ember.inject.service('panelActions'),
    _names: ['upload', 'discipline', 'basics', 'authors', 'submit'].map(str => str.capitalize()),

    // Data for project picker; tracked internally on load
    user: null,
    userNodes: Ember.A(),
    userNodesLoaded: false,

    // Information about the thing to be turned into a preprint
    node: null, // Project or component containing the preprint
    file: null, // Preuploaded file - file that has been dragged to dropzone, but not uploaded to node.
    selectedFile: null, // File that will be the preprint (already uploaded to node or selected from existing node)
    contributors: Ember.A(), // Contributors on preprint - if creating a component, contributors will be copied over from parent
    nodeTitle: null, // Preprint title
    fileAndNodeLocked: false, // After advancing beyond Step 1: Upload on Add Preprint form, both the file and node are locked
    searchResults: [], // List of users matching search query
    savingPreprint: false, // True when Share button is pressed on Add Preprint page
    showModalSharePreprint: false, // True when sharing preprint confirmation modal is displayed
    uploadSaveState: false, // True temporarily when changes have been saved in upload section
    disciplineSaveState: false, // True temporarily when changes have been saved in discipline section
    basicsSaveState: false, // True temporarily when changes have been saved in basics section
    authorsSaveState: false, // True temporarily when changes have been saved in authors section
    parentNode: null, // If component created, parentNode will be defined
    convertProjectConfirmed: false, // User has confirmed they want to convert their existing OSF project into a preprint,
    convertOrCopy: null, // Will either be 'convert' or 'copy' depending on whether user wants to use existing component or create a new component.

    isTopLevelNode: Ember.computed('node', function() {
        // Returns true if node is a top-level node
        var node = this.get('node');
        return node ? (node.get('id') === node.get('root.id')) : null;
    }),
    clearFields() {
        // Restores submit form defaults.  Called when user submits preprint, then hits back button, for example.
        this.get('panelActions').open('Upload');
        this.get('panelActions').close('Submit');

        this.setProperties(Ember.merge(
            this.get('_names').reduce((acc, name) => Ember.merge(acc, {[`${name.toLowerCase()}SaveState`]: false}), {}), {
            user: null,
            userNodes: Ember.A(),
            node: null,
            file: null,
            hasFile: false,
            selectedFile: null,
            contributors: Ember.A(),
            nodeTitle: null,
            fileAndNodeLocked: false,
            filePickerState: State.START,
            searchResults: [],
            savingPreprint: false,
            showModalSharePreprint: false,
            uploadSaveState: false,
            basicsSaveState: false,
            authorsSaveState: false,
            disciplineSaveState: false,
            parentNode: null,
            convertProjectConfirmed: false,
            basicsAbstract: null,
        }));
    },

    hasFile: Ember.computed('file', 'selectedFile', function() {
        // True if file has either been preuploaded, or already uploaded file has been selected.
        return this.get('file') !== null || this.get('selectedFile') !== null;
    }),

    ///////////////////////////////////////
    // Validation rules for form sections

    // In order to advance from upload state, node and selectedFile must have been defined, and nodeTitle must be set.
    uploadValid: Ember.computed.and('node', 'selectedFile', 'nodeTitle'),
    abstractValid: Ember.computed.alias('validations.attrs.basicsAbstract.isValid'),
    doiValid: Ember.computed.alias('validations.attrs.basicsDOI.isValid'),
    // Basics fields that are being validated are abstract and doi (title validated in upload section). If validation added for other fields, expand basicsValid definition.
    basicsValid: Ember.computed.and('abstractValid', 'doiValid'),
    // Must have at least one contributor. Backend enforces admin and bibliographic rules. If this form section is ever invalid, something has gone horribly wrong.
    authorsValid: Ember.computed.bool('contributors.length'),
    // Must select at least one subject.
    disciplineValid: Ember.computed.notEmpty('model.subjects'),
    // All form sections are valid and preprint can be shared.
    allSectionsValid: Ember.computed('uploadValid', 'basicsValid', 'authorsValid', 'disciplineValid', function() {
        return this.get('uploadValid') && this.get('basicsValid') && this.get('authorsValid') && this.get('disciplineValid');
    }),

    ////////////////////////////////////////////////////
    // Fields used in the "basics" section of the form.
    // Proxy for "basics" section, to support autosave when fields change (created when model selected)
    basicsModel: Ember.computed.alias('node'),
    basicsTitle: Ember.computed('node', function() {
        var node = this.get('node');
        return node ? node.get('title') : null;
    }),
    basicsAbstract:  Ember.computed('node', function() {
        var node = this.get('node');
        return node ? node.get('description') : null;
    }),
    basicsTags: Ember.computed('node', function() {
        var node = this.get('node');
        // TODO: This may need to provide a default value (list)? Via default or field transform?
        return node ? node.get('tags') : null;
    }),
    basicsDOI: Ember.computed.alias('model.doi'),

    getContributors: Ember.observer('node', function() {
        // Returns all contributors of node that will be container for preprint.  Makes sequential requests to API until all pages of contributors have been loaded
        // and combines into one array

        // Cannot be called until a project has been selected!
        if (!this.get('node')) return [];

        let node = this.get('node');
        let contributors = Ember.A();
        loadAll(node, 'contributors', contributors).then(()=>
             this.set('contributors', contributors));
    }),

    isAdmin: Ember.computed('node', function() {
        // True if the current user has admin permissions
        // FIXME: Workaround for isAdmin variable not making sense until a node has been loaded
        let userPermissions = this.get('node.currentUserPermissions') || [];
        return userPermissions.indexOf(permissions.ADMIN) >= 0;
    }),

    canEdit: Ember.computed('isAdmin', 'node', function() {
        // True if the current user is and admin and the node is not a registration.
        return this.get('isAdmin') && !(this.get('node.registration'));
    }),

    actions: {
        next(currentPanelName) {
            // Open next panel
            if (currentPanelName === 'Upload' || currentPanelName === 'Basics') {
                Ember.run.scheduleOnce('afterRender', this, function() {
                    MathJax.Hub.Queue(['Typeset', MathJax.Hub, Ember.$(currentPanelName === 'Upload' ? '.preprint-header-preview' : '.abstract')[0]]);  // jshint ignore:line
                });
            }
            this.get('panelActions').open(this.get(`_names.${this.get('_names').indexOf(currentPanelName) + 1}`));
            this.send('changesSaved', currentPanelName);
        },
        nextUploadSection(currentUploadPanel, nextUploadPanel) {
            // Opens next panel within the Upload Section, Existing Workflow (Choose Project - Choose File - Organize - Finalize Upload)
            this.get('panelActions').toggle(currentUploadPanel);
            this.get('panelActions').toggle(nextUploadPanel);
        },
        changesSaved(currentPanelName) {
            // Temporarily changes panel save state to true.  Used for flashing 'Changes Saved' in UI.
            var currentPanelSaveState = currentPanelName.toLowerCase() + 'SaveState';
            this.set(currentPanelSaveState, true);
            setTimeout(() => {
                this.set(currentPanelSaveState, false);

            }, 3000);
        },

        error(error /*, transition */) {
            this.get('toast').error(error);
        },
        /*
          Upload section
         */
        changeInitialState(newState) {
            // Sets filePickerState to start, new, or existing - this is the initial decision on the form.
            this.set('filePickerState', newState);
            this.send('clearDownstreamFields', 'allUpload');
            if (newState === this.get('_State').EXISTING) {
                this.get('panelActions').open('chooseProject');
                this.get('panelActions').close('selectExistingFile');
                this.get('panelActions').close('uploadNewFile');
                this.get('panelActions').close('organize');
                this.get('panelActions').close('finalizeUpload');
            }
        },
        lockFileAndNode() {
            // Locks file and node so that they cannot be modified.  Occurs after upload step is complete.
            this.set('fileAndNodeLocked', true);
        },
        finishUpload() {
            // Locks file and node and advances to next form section.
            this.send('lockFileAndNode');
            this.send('next', this.get('_names.0'));
        },
        existingNodeExistingFile() {
            // Upload case for using existing node and existing file for the preprint.  If title has been edited, updates title.
            var node = this.get('node');
            if (node.title !== this.get('nodeTitle')) {
                node.set('title', this.get('nodeTitle'));
                node.save().then(() => {
                    this.send('finishUpload');
                });
            } else {
                this.send('finishUpload');
            }
        },
        createComponentCopyFile() {
            // Upload case for using a new component and an existing file for the preprint. Creates a component and then copies
            // file from parent node to new component.

            var node = this.get('node');
            node.addChild(this.get('nodeTitle')).then(child => {
                this.set('parentNode', node);
                this.set('node', child);
                child.get('files').then((providers) => {
                    var osfstorage = providers.findBy('name', 'osfstorage');
                    this.get('fileManager').copy(this.get('selectedFile'), osfstorage, {data: {resource: child.id}}).then((copiedFile) => {
                        this.set('selectedFile', copiedFile);
                    });
                }).then(() => {
                    this.get('toast').info('File copied to component!');
                    this.send('finishUpload');
                }, () => {
                    this.get('toast').info('Could not create component.');
                });
            });
        },
        editTitleNext(section) {
            // Edits title when user returns to upload section after upload section has already been completed.
            this.set('node.title', this.get('nodeTitle'));
            Ember.run.scheduleOnce('afterRender', this, function() {
                MathJax.Hub.Queue(['Typeset', MathJax.Hub, Ember.$('.preprint-header-preview')[0]]);  // jshint ignore:line
            });
            this.send('next', section);
        },
        selectExistingFile(file) {
            // Takes file chosen from file-browser and sets equal to selectedFile. This file will become the preprint.
            this.set('selectedFile', file);
        },
        clearDownstreamFields(section) {
            //If user goes back and changes a section inside Upload, all fields downstream of that section need to clear.
            switch (section) {
                case 'allUpload':
                    this.set('node', null);
                    this.set('selectedFile', null);
                    this.set('hasFile', false);
                    this.set('file', null);
                    this.set('convertOrCopy', null);
                    this.set('nodeTitle', null);
                    break;
                case 'belowNode':
                    this.set('selectedFile', null);
                    this.set('hasFile', false);
                    this.set('file', null);
                    this.set('convertOrCopy', null);
                    this.set('nodeTitle', null);
                    break;
                case 'belowFile': {
                    this.set('convertOrCopy', null);
                    this.set('nodeTitle', null);
                    break;
                }
                case 'belowConvertOrCopy': {
                    this.set('nodeTitle', null);
                    break;
                }
            }
        },
        /*
          Basics section
         */
        saveBasics() {
            // Save the model associated with basics field, then advance to next panel
            // If save fails, do not transition
            this.send('saveAbstract');
            this.send('next', this.get('_names.2'));
        },

        saveAbstract() {
            let node = this.get('node');
            node.set('description', this.get('basicsAbstract'));
            node.save()
                .catch(() => this.send('error', 'Could not save information; please try again'));
        },

        saveSubjects(subjects) {
            // If save fails, do not transition
            this.set('model.subjects', subjects);
        },

        subjectsNext() {
            this.send('next', this.get('_names.1'));
        },
        /**
         * findContributors method.  Queries APIv2 users endpoint on any of a set of name fields.  Fetches specified page of results.
         *
         * @method findContributors
         * @param {String} query ID of user that will be a contributor on the node
         * @param {Integer} page Page number of results requested
         * @return {User[]} Returns specified page of user records matching query
         */
        findContributors(query, page) {
            return this.store.query('user', {
                filter: {
                    'full_name,given_name,middle_names,family_name': query
                },
                page: page
            }).then((contributors) => {
                this.set('searchResults', contributors);
                return contributors;
            }).catch(() => {
                this.get('toast').error('Could not perform search query.');
                this.highlightSuccessOrFailure('author-search-box', this, 'error');
            });
        },
        /**
        * highlightSuccessOrFailure method. Element with specified ID flashes green or red depending on response success.
        *
        * @method highlightSuccessOrFailure
        * @param {string} elementId Element ID to change color
        * @param {Object} context "this" scope
        * @param {string} status "success" or "error"
        */
        highlightSuccessOrFailure(elementId, context, status) {
            Ember.run.next(Ember.Object.create({ elementId: elementId, context: context }), function() {
                var elementId = this.elementId;
                var _this = this.context;

                var highlightClass =  status === 'success' ? 'successHighlight' : 'errorHighlight';

                _this.$('#' + elementId).addClass(highlightClass);

                setTimeout(() => {
                    _this.$('#' + elementId).removeClass(highlightClass);
                }, 2000);
            });
        },
        /*
          Submit tab actions
         */
        toggleSharePreprintModal() {
            // Toggles display of share preprint modal
            this.toggleProperty('showModalSharePreprint');
        },
        savePreprint() {
            // Converts 'node' into a preprint, with its primaryFile as the 'selectedFile'.
            // TODO: Check validation status of all sections before submitting
            // TODO: Make sure subjects is working so request doesn't get rejected
            // TODO: Test and get this code working
            let model = this.get('model');
            model.setProperties({
                id: this.get('node.id'),
                primaryFile: this.get('selectedFile')
            });
            this.set('savingPreprint', true);
            if (model.get('doi') === '') {
                model.set('doi', undefined);
            }
            return model.save()
                // Ember data is not worth the time investment currently
                .then(() =>  this.store.adapterFor('preprint').ajax(model.get('links.relationships.providers.links.self.href'), 'PATCH', {
                    data: {
                        data: [{
                            type: 'preprint_providers',
                            id: config.PREPRINTS.provider,
                        }]
                    }
                }))
                .then(() => model.get('providers'))
                .then(() => this.transitionToRoute('content', model));
        },
    }
});
<commit_msg>For upload section to be valid, file and node must be locked.<commit_after>import Ember from 'ember';
import config from 'ember-get-config';

import { validator, buildValidations } from 'ember-cp-validations';

import permissions from 'ember-osf/const/permissions';
import NodeActionsMixin from 'ember-osf/mixins/node-actions';
import TaggableMixin from 'ember-osf/mixins/taggable-mixin';

import loadAll from 'ember-osf/utils/load-relationship';

// Enum of available upload states
export const State = Object.freeze(Ember.Object.create({
    START: 'start',
    NEW: 'new',
    EXISTING: 'existing'
}));

/*****************************
  Form data and validations
 *****************************/
const BasicsValidations = buildValidations({
    basicsAbstract: {
        description: 'Abstract',
        validators: [
            validator('presence', true),
            validator('length', {
                // currently min of 20 characters -- this is what arXiv has as the minimum length of an abstract
                min: 20,
                max: 5000
            })
        ]
    },
    basicsDOI: {
        description: 'DOI',
        validators: [
            validator('format', {
                // Simplest regex- try not to diverge too much from the backend
                regex: /^10\.\S+\//,
                allowBlank: true,
                message: 'Please use a valid {description}'
            })
        ]
    }
});

/**
 * "Add preprint" page definitions
 */
export default Ember.Controller.extend(BasicsValidations, NodeActionsMixin, TaggableMixin, {
    _State: State,
    filePickerState: State.START,
    fileManager: Ember.inject.service(),
    toast: Ember.inject.service('toast'),
    panelActions: Ember.inject.service('panelActions'),
    _names: ['upload', 'discipline', 'basics', 'authors', 'submit'].map(str => str.capitalize()),

    // Data for project picker; tracked internally on load
    user: null,
    userNodes: Ember.A(),
    userNodesLoaded: false,

    // Information about the thing to be turned into a preprint
    node: null, // Project or component containing the preprint
    file: null, // Preuploaded file - file that has been dragged to dropzone, but not uploaded to node.
    selectedFile: null, // File that will be the preprint (already uploaded to node or selected from existing node)
    contributors: Ember.A(), // Contributors on preprint - if creating a component, contributors will be copied over from parent
    nodeTitle: null, // Preprint title
    fileAndNodeLocked: false, // After advancing beyond Step 1: Upload on Add Preprint form, both the file and node are locked
    searchResults: [], // List of users matching search query
    savingPreprint: false, // True when Share button is pressed on Add Preprint page
    showModalSharePreprint: false, // True when sharing preprint confirmation modal is displayed
    uploadSaveState: false, // True temporarily when changes have been saved in upload section
    disciplineSaveState: false, // True temporarily when changes have been saved in discipline section
    basicsSaveState: false, // True temporarily when changes have been saved in basics section
    authorsSaveState: false, // True temporarily when changes have been saved in authors section
    parentNode: null, // If component created, parentNode will be defined
    convertProjectConfirmed: false, // User has confirmed they want to convert their existing OSF project into a preprint,
    convertOrCopy: null, // Will either be 'convert' or 'copy' depending on whether user wants to use existing component or create a new component.

    isTopLevelNode: Ember.computed('node', function() {
        // Returns true if node is a top-level node
        var node = this.get('node');
        return node ? (node.get('id') === node.get('root.id')) : null;
    }),
    clearFields() {
        // Restores submit form defaults.  Called when user submits preprint, then hits back button, for example.
        this.get('panelActions').open('Upload');
        this.get('panelActions').close('Submit');

        this.setProperties(Ember.merge(
            this.get('_names').reduce((acc, name) => Ember.merge(acc, {[`${name.toLowerCase()}SaveState`]: false}), {}), {
            user: null,
            userNodes: Ember.A(),
            node: null,
            file: null,
            hasFile: false,
            selectedFile: null,
            contributors: Ember.A(),
            nodeTitle: null,
            fileAndNodeLocked: false,
            filePickerState: State.START,
            searchResults: [],
            savingPreprint: false,
            showModalSharePreprint: false,
            uploadSaveState: false,
            basicsSaveState: false,
            authorsSaveState: false,
            disciplineSaveState: false,
            parentNode: null,
            convertProjectConfirmed: false,
            basicsAbstract: null,
        }));
    },

    hasFile: Ember.computed('file', 'selectedFile', function() {
        // True if file has either been preuploaded, or already uploaded file has been selected.
        return this.get('file') !== null || this.get('selectedFile') !== null;
    }),

    ///////////////////////////////////////
    // Validation rules for form sections

    // In order to advance from upload state, node and selectedFile must have been defined, and nodeTitle must be set.
    uploadValid: Ember.computed.and('node', 'selectedFile', 'nodeTitle', 'fileAndNodeLocked'),
    abstractValid: Ember.computed.alias('validations.attrs.basicsAbstract.isValid'),
    doiValid: Ember.computed.alias('validations.attrs.basicsDOI.isValid'),
    // Basics fields that are being validated are abstract and doi (title validated in upload section). If validation added for other fields, expand basicsValid definition.
    basicsValid: Ember.computed.and('abstractValid', 'doiValid'),
    // Must have at least one contributor. Backend enforces admin and bibliographic rules. If this form section is ever invalid, something has gone horribly wrong.
    authorsValid: Ember.computed.bool('contributors.length'),
    // Must select at least one subject.
    disciplineValid: Ember.computed.notEmpty('model.subjects'),
    // All form sections are valid and preprint can be shared.
    allSectionsValid: Ember.computed('uploadValid', 'basicsValid', 'authorsValid', 'disciplineValid', function() {
        return this.get('uploadValid') && this.get('basicsValid') && this.get('authorsValid') && this.get('disciplineValid');
    }),

    ////////////////////////////////////////////////////
    // Fields used in the "basics" section of the form.
    // Proxy for "basics" section, to support autosave when fields change (created when model selected)
    basicsModel: Ember.computed.alias('node'),
    basicsTitle: Ember.computed('node', function() {
        var node = this.get('node');
        return node ? node.get('title') : null;
    }),
    basicsAbstract:  Ember.computed('node', function() {
        var node = this.get('node');
        return node ? node.get('description') : null;
    }),
    basicsTags: Ember.computed('node', function() {
        var node = this.get('node');
        // TODO: This may need to provide a default value (list)? Via default or field transform?
        return node ? node.get('tags') : null;
    }),
    basicsDOI: Ember.computed.alias('model.doi'),

    getContributors: Ember.observer('node', function() {
        // Returns all contributors of node that will be container for preprint.  Makes sequential requests to API until all pages of contributors have been loaded
        // and combines into one array

        // Cannot be called until a project has been selected!
        if (!this.get('node')) return [];

        let node = this.get('node');
        let contributors = Ember.A();
        loadAll(node, 'contributors', contributors).then(()=>
             this.set('contributors', contributors));
    }),

    isAdmin: Ember.computed('node', function() {
        // True if the current user has admin permissions
        // FIXME: Workaround for isAdmin variable not making sense until a node has been loaded
        let userPermissions = this.get('node.currentUserPermissions') || [];
        return userPermissions.indexOf(permissions.ADMIN) >= 0;
    }),

    canEdit: Ember.computed('isAdmin', 'node', function() {
        // True if the current user is and admin and the node is not a registration.
        return this.get('isAdmin') && !(this.get('node.registration'));
    }),

    actions: {
        next(currentPanelName) {
            // Open next panel
            if (currentPanelName === 'Upload' || currentPanelName === 'Basics') {
                Ember.run.scheduleOnce('afterRender', this, function() {
                    MathJax.Hub.Queue(['Typeset', MathJax.Hub, Ember.$(currentPanelName === 'Upload' ? '.preprint-header-preview' : '.abstract')[0]]);  // jshint ignore:line
                });
            }
            this.get('panelActions').open(this.get(`_names.${this.get('_names').indexOf(currentPanelName) + 1}`));
            this.send('changesSaved', currentPanelName);
        },
        nextUploadSection(currentUploadPanel, nextUploadPanel) {
            // Opens next panel within the Upload Section, Existing Workflow (Choose Project - Choose File - Organize - Finalize Upload)
            this.get('panelActions').toggle(currentUploadPanel);
            this.get('panelActions').toggle(nextUploadPanel);
        },
        changesSaved(currentPanelName) {
            // Temporarily changes panel save state to true.  Used for flashing 'Changes Saved' in UI.
            var currentPanelSaveState = currentPanelName.toLowerCase() + 'SaveState';
            this.set(currentPanelSaveState, true);
            setTimeout(() => {
                this.set(currentPanelSaveState, false);

            }, 3000);
        },

        error(error /*, transition */) {
            this.get('toast').error(error);
        },
        /*
          Upload section
         */
        changeInitialState(newState) {
            // Sets filePickerState to start, new, or existing - this is the initial decision on the form.
            this.set('filePickerState', newState);
            this.send('clearDownstreamFields', 'allUpload');
            if (newState === this.get('_State').EXISTING) {
                this.get('panelActions').open('chooseProject');
                this.get('panelActions').close('selectExistingFile');
                this.get('panelActions').close('uploadNewFile');
                this.get('panelActions').close('organize');
                this.get('panelActions').close('finalizeUpload');
            }
        },
        lockFileAndNode() {
            // Locks file and node so that they cannot be modified.  Occurs after upload step is complete.
            this.set('fileAndNodeLocked', true);
        },
        finishUpload() {
            // Locks file and node and advances to next form section.
            this.send('lockFileAndNode');
            this.send('next', this.get('_names.0'));
        },
        existingNodeExistingFile() {
            // Upload case for using existing node and existing file for the preprint.  If title has been edited, updates title.
            var node = this.get('node');
            if (node.title !== this.get('nodeTitle')) {
                node.set('title', this.get('nodeTitle'));
                node.save().then(() => {
                    this.send('finishUpload');
                });
            } else {
                this.send('finishUpload');
            }
        },
        createComponentCopyFile() {
            // Upload case for using a new component and an existing file for the preprint. Creates a component and then copies
            // file from parent node to new component.

            var node = this.get('node');
            node.addChild(this.get('nodeTitle')).then(child => {
                this.set('parentNode', node);
                this.set('node', child);
                child.get('files').then((providers) => {
                    var osfstorage = providers.findBy('name', 'osfstorage');
                    this.get('fileManager').copy(this.get('selectedFile'), osfstorage, {data: {resource: child.id}}).then((copiedFile) => {
                        this.set('selectedFile', copiedFile);
                    });
                }).then(() => {
                    this.get('toast').info('File copied to component!');
                    this.send('finishUpload');
                }, () => {
                    this.get('toast').info('Could not create component.');
                });
            });
        },
        editTitleNext(section) {
            // Edits title when user returns to upload section after upload section has already been completed.
            this.set('node.title', this.get('nodeTitle'));
            Ember.run.scheduleOnce('afterRender', this, function() {
                MathJax.Hub.Queue(['Typeset', MathJax.Hub, Ember.$('.preprint-header-preview')[0]]);  // jshint ignore:line
            });
            this.send('next', section);
        },
        selectExistingFile(file) {
            // Takes file chosen from file-browser and sets equal to selectedFile. This file will become the preprint.
            this.set('selectedFile', file);
        },
        clearDownstreamFields(section) {
            //If user goes back and changes a section inside Upload, all fields downstream of that section need to clear.
            switch (section) {
                case 'allUpload':
                    this.set('node', null);
                    this.set('selectedFile', null);
                    this.set('hasFile', false);
                    this.set('file', null);
                    this.set('convertOrCopy', null);
                    this.set('nodeTitle', null);
                    break;
                case 'belowNode':
                    this.set('selectedFile', null);
                    this.set('hasFile', false);
                    this.set('file', null);
                    this.set('convertOrCopy', null);
                    this.set('nodeTitle', null);
                    break;
                case 'belowFile': {
                    this.set('convertOrCopy', null);
                    this.set('nodeTitle', null);
                    break;
                }
                case 'belowConvertOrCopy': {
                    this.set('nodeTitle', null);
                    break;
                }
            }
        },
        /*
          Basics section
         */
        saveBasics() {
            // Save the model associated with basics field, then advance to next panel
            // If save fails, do not transition
            this.send('saveAbstract');
            this.send('next', this.get('_names.2'));
        },

        saveAbstract() {
            let node = this.get('node');
            node.set('description', this.get('basicsAbstract'));
            node.save()
                .catch(() => this.send('error', 'Could not save information; please try again'));
        },

        saveSubjects(subjects) {
            // If save fails, do not transition
            this.set('model.subjects', subjects);
        },

        subjectsNext() {
            this.send('next', this.get('_names.1'));
        },
        /**
         * findContributors method.  Queries APIv2 users endpoint on any of a set of name fields.  Fetches specified page of results.
         *
         * @method findContributors
         * @param {String} query ID of user that will be a contributor on the node
         * @param {Integer} page Page number of results requested
         * @return {User[]} Returns specified page of user records matching query
         */
        findContributors(query, page) {
            return this.store.query('user', {
                filter: {
                    'full_name,given_name,middle_names,family_name': query
                },
                page: page
            }).then((contributors) => {
                this.set('searchResults', contributors);
                return contributors;
            }).catch(() => {
                this.get('toast').error('Could not perform search query.');
                this.highlightSuccessOrFailure('author-search-box', this, 'error');
            });
        },
        /**
        * highlightSuccessOrFailure method. Element with specified ID flashes green or red depending on response success.
        *
        * @method highlightSuccessOrFailure
        * @param {string} elementId Element ID to change color
        * @param {Object} context "this" scope
        * @param {string} status "success" or "error"
        */
        highlightSuccessOrFailure(elementId, context, status) {
            Ember.run.next(Ember.Object.create({ elementId: elementId, context: context }), function() {
                var elementId = this.elementId;
                var _this = this.context;

                var highlightClass =  status === 'success' ? 'successHighlight' : 'errorHighlight';

                _this.$('#' + elementId).addClass(highlightClass);

                setTimeout(() => {
                    _this.$('#' + elementId).removeClass(highlightClass);
                }, 2000);
            });
        },
        /*
          Submit tab actions
         */
        toggleSharePreprintModal() {
            // Toggles display of share preprint modal
            this.toggleProperty('showModalSharePreprint');
        },
        savePreprint() {
            // Converts 'node' into a preprint, with its primaryFile as the 'selectedFile'.
            // TODO: Check validation status of all sections before submitting
            // TODO: Make sure subjects is working so request doesn't get rejected
            // TODO: Test and get this code working
            let model = this.get('model');
            model.setProperties({
                id: this.get('node.id'),
                primaryFile: this.get('selectedFile')
            });
            this.set('savingPreprint', true);
            if (model.get('doi') === '') {
                model.set('doi', undefined);
            }
            return model.save()
                // Ember data is not worth the time investment currently
                .then(() =>  this.store.adapterFor('preprint').ajax(model.get('links.relationships.providers.links.self.href'), 'PATCH', {
                    data: {
                        data: [{
                            type: 'preprint_providers',
                            id: config.PREPRINTS.provider,
                        }]
                    }
                }))
                .then(() => model.get('providers'))
                .then(() => this.transitionToRoute('content', model));
        },
    }
});
<|endoftext|>


<commit_before>// ## Tweet Controller
const createPagination = require("./analytics").createPagination;
const mongoose = require("mongoose");
const Tweet = mongoose.model("Tweet");
const User = mongoose.model("User");
const Analytics = mongoose.model("Analytics");
const _ = require("underscore");
const logger = require("../middlewares/logger");

exports.tweet = (req, res, next, id) => {
  Tweet.load(id, (err, tweet) => {
    if (err) {
      return next(err);
    }
    if (!tweet) {
      return next(new Error("Failed to load tweet" + id));
    }
    req.tweet = tweet;
    next();
  });
};

// ### Create a Tweet
exports.create = (req, res) => {
  const tweet = new Tweet(req.body);
  tweet.user = req.user;
  tweet.uploadAndSave({}, err => {
    if (err) {
      res.render("pages/500", { error: err });
    } else {
      res.redirect("/");
    }
  });
};

// ### Update a tweet
exports.update = (req, res) => {
  let tweet = req.tweet;
  tweet = _.extend(tweet, { body: req.body.tweet });
  tweet.uploadAndSave({}, err => {
    if (err) {
      return res.render("pages/500", { error: err });
    }
    res.redirect("/");
  });
};

// ### Delete a tweet
exports.destroy = (req, res) => {
  const tweet = req.tweet;
  tweet.remove(err => {
    if (err) {
      return res.render("pages/500");
    }
    res.redirect("/");
  });
};

exports.index = (req, res) => {
  const page = (req.query.page > 0 ? req.query.page : 1) - 1;
  const perPage = 10;
  const options = {
    perPage: perPage,
    page: page
  };
  let followingCount = req.user.following.length;
  let followerCount = req.user.followers.length;
  let tweets, tweetCount, pageViews, analytics, pagination;
  User.countUserTweets(req.user._id).then(result => {
    tweetCount = result;
  });
  Tweet.list(options)
    .then(result => {
      tweets = result;
      return Tweet.countTotalTweets();
    })
    .then(result => {
      pageViews = result;
      pagination = createPagination(
        req,
        Math.ceil(pageViews / perPage),
        page + 1
      );
      return Analytics.list({ perPage: 15 });
    })
    .then(result => {
      analytics = result;
      res.render("pages/index", {
        title: "List of Tweets",
        tweets: tweets,
        analytics: analytics,
        page: page + 1,
        tweetCount: tweetCount,
        pagination: pagination,
        followerCount: followerCount,
        followingCount: followingCount,
        pages: Math.ceil(pageViews / perPage)
      });
    })
    .catch(error => {
      logger.error(error);
      res.render("pages/500");
    });
};
<commit_msg>Added function to parse tweets<commit_after>// ## Tweet Controller
const createPagination = require("./analytics").createPagination;
const mongoose = require("mongoose");
const Tweet = mongoose.model("Tweet");
const User = mongoose.model("User");
const Analytics = mongoose.model("Analytics");
const _ = require("underscore");
const logger = require("../middlewares/logger");

exports.tweet = (req, res, next, id) => {
  Tweet.load(id, (err, tweet) => {
    if (err) {
      return next(err);
    }
    if (!tweet) {
      return next(new Error("Failed to load tweet" + id));
    }
    req.tweet = tweet;
    next();
  });
};

// ### Create a Tweet
exports.create = (req, res) => {
  const tweet = new Tweet(req.body);
  tweet.user = req.user;
  tweet.uploadAndSave({}, err => {
    if (err) {
      res.render("pages/500", { error: err });
    } else {
      res.redirect("/");
    }
  });
};

// ### Update a tweet
exports.update = (req, res) => {
  let tweet = req.tweet;
  tweet = _.extend(tweet, { body: req.body.tweet });
  tweet.uploadAndSave({}, err => {
    if (err) {
      return res.render("pages/500", { error: err });
    }
    res.redirect("/");
  });
};

// ### Delete a tweet
exports.destroy = (req, res) => {
  const tweet = req.tweet;
  tweet.remove(err => {
    if (err) {
      return res.render("pages/500");
    }
    res.redirect("/");
  });
};

exports.index = (req, res) => {
  const page = (req.query.page > 0 ? req.query.page : 1) - 1;
  const perPage = 10;
  const options = {
    perPage: perPage,
    page: page
  };
  let followingCount = req.user.following.length;
  let followerCount = req.user.followers.length;
  let tweets, tweetCount, pageViews, analytics, pagination;
  User.countUserTweets(req.user._id).then(result => {
    tweetCount = result;
  });
  Tweet.list(options)
    .then(result => {
      tweets = result;
      return Tweet.countTotalTweets();
    })
    .then(result => {
      pageViews = result;
      pagination = createPagination(
        req,
        Math.ceil(pageViews / perPage),
        page + 1
      );
      return Analytics.list({ perPage: 15 });
    })
    .then(result => {
      analytics = result;
      res.render("pages/index", {
        title: "List of Tweets",
        tweets: tweets,
        analytics: analytics,
        page: page + 1,
        tweetCount: tweetCount,
        pagination: pagination,
        followerCount: followerCount,
        followingCount: followingCount,
        pages: Math.ceil(pageViews / perPage)
      });
    })
    .catch(error => {
      logger.error(error);
      res.render("pages/500");
    });
};

// Parse Hashtag
exports.parseHashtag = (inputText) => {
  var regex = /(?:^|\s)(?:#)([a-zA-Z\d]+)/g;
  var matches = [];
  var match;
  while ((match = regex.exec(inputText)) !== null) {
    matches.push(match[1]);
  }
  return matches;
};

<|endoftext|>


<commit_before>'use strict';
/**
 * Copyright (c) 2016, WikiWeb
*/
const mongoose = require('mongoose')
const Entity = mongoose.model('Entity');
const Edge = require('../models/edge');
const utils = require('../../lib/utils')
const User = mongoose.model('User');
const _ = require('lodash');
const pageSearch = require('../../lib/pageSearch')

/**
 * loadApi
 */
exports.load = function (req, res, next, id){
  Entity.load(id, function (err, entity) {
    if (!entity || (err && err.message==='Cast to ObjectId failed')) return  res.status(404).send(utils.errsForApi('Page not found'));
    if (err) return  res.status(500).send( utils.errsForApi(err.errors || err) );
    req.entity = entity;
    Edge.getNearByNodeEdges(entity._id, function(errNear, nearByEdges){
      if (errNear) return  res.status(500).send( utils.errsForApi(errNear.errors || errNear) );
      req.nearByEdges = nearByEdges;

      Edge.getNode(entity._id, function(errNodeEdge, edges){
        if (errNodeEdge) return  res.status(500).send( utils.errsForApi(errNodeEdge.errors || errNodeEdge) );
        req.edges = edges
        next();
      })
    })
  });
};

/**
* Entity API for Nodes
 */
exports.getEntityController = function (req, res) {
  const entity = req.entity;

  const edges = _.chain(req.edges)
    .groupBy('_idNode')
    .map(function (edgesGrouped ){
        const edgeSorted = _.sortBy(edgesGrouped,'createdAt');
        return {
          _idNode: edgeSorted[0]._idNode,
          _idLink: edgeSorted[0]._idLink,
          createdAt: edgeSorted[0].createdAt,
          edges:edgeSorted
        };
    }).value();
  const nearByEdges = req.nearByEdges;
  const entityIds = _.map(req.edges, '_idNode');
  const userIds = _.map(req.edges, '_idUser');
  const entityIdsRelated = _.chain(nearByEdges)
    .map('nodes')
    .flatten()
    .map('_id')
    .value();
  const userIdsRelated = _.chain(nearByEdges)
    .map('edges')
    .flatten()
    .map('properties.userId')
    .reject(_.isUndefined)
    .value();

  if (!entity) {
    res.status(404).send(utils.errsForApi('Node not found!!'));
  } else {
    const object = entity.toJSON();

    Edge.getNodeCount(entityIds.concat(entity.id).concat(entityIdsRelated), function (err, entityCount){
      if (err) return  res.status(500).send( utils.errsForApi(err.errors || err) );

      Entity.find(
        { _id: {$in:  entityIds.concat(entityIdsRelated) }},
        '_id title description createdAt canonicalLink queryLink faviconCDN isConnected image imageCDN')
      .exec(function(err, entities){

        User.find(
          { _id: {$in: userIds.concat(userIdsRelated) }},
          'name username twitter'
        )
        .exec(function(err, users){
          object.entityCount =  entityCount[entity.id]?entityCount[entity.id].length:0;
          object.nearByEdges = nearByEdges.map(function(near){
            const nodes = near.nodes.map(function(node){
              const _id = node._id;
              return  {
                node: _.find(entities, { id: _id}),
                entityCount: entityCount[_id]?entityCount[_id].length:0
              }
            });

            const edges = near.edges.map(function(edge){
              const userId = edge.properties.userId
              if (userId!=undefined){
                edge.properties.userId = _.find(users, { id: userId})
              }
              return edge;
            })
            return {
              nodes,
              edges
            }
          });

          object.superEdges = edges.map(function(edge, i){
            return {
              entity: _.find(entities, { id: edge._idNode}),
              entityCount: entityCount[edge._idNode].length,
              createdAt: edge.createdAt,
              edges: edge.edges.map(function (e){
                return {
                  user:_.find(users, { id: e._idUser }),
                  createdAt: e.createdAt,
                  _id: e._idLink,
                  tags: e.tags ? e.tags : []
                };
              })
            };
          });
          res.send(object);
        });
      });// Entity.find
    });// Edge.getNodeCount
  }
};

/**
 * Search API for Node and Scrape Page
 */
exports.getSearchController = function (req, res) {
  const q = req.query.q
  const user = req.user
  if (!q) return res.status(422).send(utils.errsForApi('Please Enter a URL'));

  pageSearch(q, function(err, url, resultDB, extractedPageData){
      //Handle the end
      if (err &&
         (err.status === 404 || err.code )) { //Did we get a 404 from the search. We tell the search
        console.log(err, 'getSearchController isURL = flase')
        res.send({
           node : null,
           isURL: false
        });
      } else if (err) {
        const status = err.status || 500;
        console.log(err, 'getSearchController err')
        res.status(status).json(err);
      } else {
        //Here is where we push all links to our child scrapper.
        addToScrapperQ(_.map(resultDB.links, function(link){
          return {
            href: link.href,
            fromId: resultDB.id
          }
        }))
        const payload = {
           node : resultDB.toObject(),
           isURL: true
        };
        res.send(payload);
      }
  })
};

function scraperRecursive(){
  const edgeType = 'siteEdge';
  running = true;

  const link = Q.pop()
  if (link===undefined || !link.href){// no link stop attempt for page search
    if (Q.length==0) { //Q was empty we can not continue;
      running = false
    } else {
      scraperRecursive()
    }

    return false; //Do not run the page search
  }

  pageSearch(link.href, function(err, url, resultDB, extractedPageData){
    if (!err && resultDB){
      const fromId = link.fromId
      const toId = resultDB.id
      Edge.getEdgesForPath(
        fromId,
        toId,
        function(err, resultExisting){
          if (resultExisting && resultExisting.length==0){
            Edge.createSiteEdge(
              fromId,
              toId,
              function(err, resultEdge){
                if (err) console.log(err, 'scraperRecursive getEdgesForPath')
            })
          }

        });

    } else{
      //No Opp
    }

    if (Q.length==0){
      running = false; //Stop the recursive call and set running to false
    } else {
      scraperRecursive()
    }
  });

}

function addToScrapperQ(hrefs){
  Q = Q.concat(_.uniq(hrefs))
  //console.log(Q.length,'Q is now this')
  if (running==false){
    scraperRecursive();
  }
}

let Q =[];
let running = false;
<commit_msg>linting. left undefined variables.<commit_after>'use strict';
/**
 * Copyright (c) 2016, WikiWeb
*/
const mongoose = require('mongoose');
const Entity = mongoose.model('Entity');
const Edge = require('../models/edge');
const utils = require('../../lib/utils');
const User = mongoose.model('User');
const _ = require('lodash');
const pageSearch = require('../../lib/pageSearch');

/**
 * loadApi
 */
exports.load = function (req, res, next, id){
  Entity.load(id, function (err, entity) {
    if (!entity || (err && err.message === 'Cast to ObjectId failed')) return  res.status(404).send(utils.errsForApi('Page not found'));
    if (err) return  res.status(500).send( utils.errsForApi(err.errors || err) );
    req.entity = entity;
    Edge.getNearByNodeEdges(entity._id, function (errNear, nearByEdges) {
      if (errNear) return  res.status(500).send( utils.errsForApi(errNear.errors || errNear) );
      req.nearByEdges = nearByEdges;

      Edge.getNode(entity._id, function (errNodeEdge, edges){
        if (errNodeEdge) return  res.status(500).send( utils.errsForApi(errNodeEdge.errors || errNodeEdge) );
        req.edges = edges;
        next();
      });
    });
  });
};

/**
* Entity API for Nodes
 */
exports.getEntityController = function (req, res) {
  const entity = req.entity;

  const edges = _.chain(req.edges)
    .groupBy('_idNode')
    .map(function (edgesGrouped ){
        const edgeSorted = _.sortBy(edgesGrouped,'createdAt');
        return {
          _idNode: edgeSorted[0]._idNode,
          _idLink: edgeSorted[0]._idLink,
          createdAt: edgeSorted[0].createdAt,
          edges:edgeSorted
        };
    }).value();
  const nearByEdges = req.nearByEdges;
  const entityIds = _.map(req.edges, '_idNode');
  const userIds = _.map(req.edges, '_idUser');
  const entityIdsRelated = _.chain(nearByEdges)
    .map('nodes')
    .flatten()
    .map('_id')
    .value();
  const userIdsRelated = _.chain(nearByEdges)
    .map('edges')
    .flatten()
    .map('properties.userId')
    .reject(_.isUndefined)
    .value();

  if (!entity) {
    res.status(404).send(utils.errsForApi('Node not found!!'));
  } else {
    const object = entity.toJSON();

    Edge.getNodeCount(entityIds.concat(entity.id).concat(entityIdsRelated), function (err, entityCount){
      if (err) return  res.status(500).send( utils.errsForApi(err.errors || err) );

      Entity.find(
        { _id: { $in:  entityIds.concat(entityIdsRelated) } },
        '_id title description createdAt canonicalLink queryLink faviconCDN isConnected image imageCDN')
      .exec(function (err, entities){

        User.find(
          { _id: { $in: userIds.concat(userIdsRelated) } },
          'name username twitter'
        )
        .exec(function (err, users){
          object.entityCount =  entityCount[entity.id] ? entityCount[entity.id].length : 0;
          object.nearByEdges = nearByEdges.map(function (near){
            const nodes = near.nodes.map(function (node){
              const _id = node._id;
              return  {
                node: _.find(entities, { id: _id }),
                entityCount: entityCount[_id] ? entityCount[_id].length : 0
              };
            });

            const edges = near.edges.map(function (edge){
              const userId = edge.properties.userId;
              if (userId !== undefined){
                edge.properties.userId = _.find(users, { id: userId });
              }
              return edge;
            });
            return {
              nodes,
              edges
            };
          });

          object.superEdges = edges.map(function (edge, index){
            return {
              entity: _.find(entities, { id: edge._idNode }),
              entityCount: entityCount[edge._idNode].length,
              createdAt: edge.createdAt,
              edges: edge.edges.map(function (e){
                return {
                  user:_.find(users, { id: e._idUser }),
                  createdAt: e.createdAt,
                  _id: e._idLink,
                  tags: e.tags ? e.tags : []
                };
              })
            };
          });
          res.send(object);
        });
      });// Entity.find
    });// Edge.getNodeCount
  }
};

/**
 * Search API for Node and Scrape Page
 */
exports.getSearchController = function (req, res) {
  const q = req.query.q;
  const user = req.user;
  if (!q) return res.status(422).send(utils.errsForApi('Please Enter a URL'));

  pageSearch(q, function (err, url, resultDB, extractedPageData){
      // Handle the end
      if (err &&
         (err.status === 404 || err.code )) { // Did we get a 404 from the search. We tell the search
        console.log(err, 'getSearchController isURL = false');
        res.send({
           node : null,
           isURL: false
        });
      } else if (err) {
        const status = err.status || 500;
        console.log(err, 'getSearchController err');
        res.status(status).json(err);
      } else {
        // Here is where we push all links to our child scrapper.
        addToScrapperQ(_.map(resultDB.links, function (link){
          return {
            href: link.href,
            fromId: resultDB.id
          };
        }));
        const payload = {
           node : resultDB.toObject(),
           isURL: true
        };
        res.send(payload);
      }
  });
};

function scraperRecursive (){
  const edgeType = 'siteEdge';
  running = true;

  const link = Q.pop();
  if (link === undefined || !link.href){// no link stop attempt for page search
    if (Q.length === 0) { // Q was empty we can not continue;
      running = false;
    } else {
      scraperRecursive();
    }

    return false; // Do not run the page search
  }

  pageSearch(link.href, function (err, url, resultDB, extractedPageData){
    if (!err && resultDB){
      const fromId = link.fromId;
      const toId = resultDB.id;
      Edge.getEdgesForPath(
        fromId,
        toId,
        function (err, resultExisting){
          if (resultExisting && resultExisting.length === 0){
            Edge.createSiteEdge(
              fromId,
              toId,
              function (err, resultEdge){
                if (err) console.log(err, 'scraperRecursive getEdgesForPath');
            });
          }

        });

    } else {
      // No Opp
    }

    if (Q.length === 0){
      running = false; // Stop the recursive call and set running to false
    } else {
      scraperRecursive();
    }
  });

}

function addToScrapperQ (hrefs){
  Q = Q.concat(_.uniq(hrefs));
  // console.log(Q.length,'Q is now this')
  if (running === false){
    scraperRecursive();
  }
}

let Q = [];
let running = false;
<|endoftext|>


<commit_before>define(['./module.js'], function(module) {
  return module.directive('showtab', ['$location', '$route', '$rootScope',
    function($location, $route, $rootScope) {
      var linkSelector = 'a',
        firstLink = 'li:first a',
        lastRoute = $location.path(),
        currentCrumb;

      function highlightCurrentTab(el, selectedTab, scope) {
        var $el = el.find(!selectedTab ? firstLink : linkSelector + '[href="#' + selectedTab + '"]');
        $el.tab('show');
        updateCurrentCrumb($el.text(), $el.attr('href'));
        var clickCallback = scope.onClick();
        $rootScope.$emit('breadcrumbs:add', currentCrumb.title, currentCrumb.url);
        return clickCallback && clickCallback(selectedTab);
      }

      function updateCurrentCrumb(title, url) {
        currentCrumb = {
          title: title,
          url: url
        };
      }

      function getSelectedTab() {
        var searchParams = $location.search();
        // we check for isString because if the someone input blah?tab= and no value
        // $location.search will return {tab: true}, so instead we force a null
        // to show there's no tab selected.
        return angular.isString(searchParams.tab) && searchParams.tab || null;
      }

      return {
        scope: {
          onClick: '&'
        },
        restrict: 'A',
        link: function(scope, element, attrs) {

          element.find(linkSelector).click(function(event) {
            event.preventDefault();
            var $a = angular.element(event.target);

            // push the new route onto history but don't reload. the locationChangeSuccess
            // event will handle the rest. This covers both cases where we load fresh
            // or a change is triggered by user click.
            // PLEASE DON'T BE CLEVER AND SWITCH THIS TO USING HASHES. THIS WILL FAIL BECAUSE
            // OF Html5Mode(true) IN app_routes.js.
            scope.$apply(function() {
              $location.search({
                'tab': $a.attr('href').slice(1)
              });
            });
          });

          scope.$on('$locationChangeSuccess', function(event, current, next) {
            var selectedTab = getSelectedTab();
            if ($location.path() !== lastRoute) {
              lastRoute = $location.path();
              return;
            }
            highlightCurrentTab(element, selectedTab, scope);
          });

          scope.$on('$destroy', function() {
            element.find(linkSelector).off();
          });

          // update lastRoute
          lastRoute = $location.path();
          // Kick off the process on page load because locationChange doesn't
          // fire on first load.
          highlightCurrentTab(element, getSelectedTab(), scope);
        }
      };
    }
  ]);
});<commit_msg>adjust comment<commit_after>define(['./module.js'], function(module) {
  return module.directive('showtab', ['$location', '$route', '$rootScope',
    function($location, $route, $rootScope) {
      var linkSelector = 'a',
        firstLink = 'li:first a',
        lastRoute = $location.path(),
        currentCrumb;

      function highlightCurrentTab(el, selectedTab, scope) {
        var $el = el.find(!selectedTab ? firstLink : linkSelector + '[href="#' + selectedTab + '"]');
        $el.tab('show');
        updateCurrentCrumb($el.text(), $el.attr('href'));
        var clickCallback = scope.onClick();
        $rootScope.$emit('breadcrumbs:add', currentCrumb.title, currentCrumb.url);
        return clickCallback && clickCallback(selectedTab);
      }

      function updateCurrentCrumb(title, url) {
        currentCrumb = {
          title: title,
          url: url
        };
      }

      function getSelectedTab() {
        var searchParams = $location.search();
        // we check for isString because if the someone input blah?tab= and no value
        // $location.search will return {tab: true}, so instead we force a null
        // to show there's no tab selected.
        return angular.isString(searchParams.tab) && searchParams.tab || null;
      }

      return {
        scope: {
          onClick: '&'
        },
        restrict: 'A',
        link: function(scope, element, attrs) {

          element.find(linkSelector).click(function(event) {
            event.preventDefault();
            var $a = angular.element(event.target);

            // push the new route onto history but don't reload. the locationChangeSuccess
            // event will handle the rest. This covers both cases where we load fresh
            // or a change is triggered by user click.
            // PLEASE DON'T BE CLEVER AND SWITCH THIS TO USING HASHES. THIS WILL FAIL BECAUSE
            // OF Html5Mode(true) IN routes.js.
            scope.$apply(function() {
              $location.search({
                'tab': $a.attr('href').slice(1)
              });
            });
          });

          scope.$on('$locationChangeSuccess', function(event, current, next) {
            var selectedTab = getSelectedTab();
            if ($location.path() !== lastRoute) {
              lastRoute = $location.path();
              return;
            }
            highlightCurrentTab(element, selectedTab, scope);
          });

          scope.$on('$destroy', function() {
            element.find(linkSelector).off();
          });

          // update lastRoute
          lastRoute = $location.path();
          // Kick off the process on page load because locationChange doesn't
          // fire on first load.
          highlightCurrentTab(element, getSelectedTab(), scope);
        }
      };
    }
  ]);
});<|endoftext|>


<commit_before>#!/usr/bin/env node
import '../../server/env';

import moment from 'moment';

import { Service as ConnectedAccountServices } from '../../server/constants/connected_account';
import { getFxRate } from '../../server/lib/currency';
import logger from '../../server/lib/logger';
import * as privacyLib from '../../server/lib/privacy';
import models from '../../server/models';
import privacy from '../../server/paymentProviders/privacy';

const DRY = process.env.DRY;

export async function run() {
  logger.info('Reconciling Privacy.com Credit Card transactions...');
  if (DRY) {
    logger.warn(`Running DRY, no changes to the DB`);
  }

  const connectedAccounts = await models.ConnectedAccount.findAll({
    where: { service: ConnectedAccountServices.PRIVACY },
  });
  logger.info(`Found ${connectedAccounts.length} connected Privacy accounts...`);

  for (const connectedAccount of connectedAccounts) {
    const host = await models.Collective.findByPk(connectedAccount.CollectiveId);
    const cards = await models.VirtualCard.findAll({ where: { HostCollectiveId: host.id } });
    logger.info(`Found ${cards.length} cards connected to host #${connectedAccount.CollectiveId} ${host.slug}...`);

    for (const card of cards) {
      const lastSyncedTransaction = await models.Expense.findOne({
        where: { VirtualCardId: card.id },
        order: [['createdAt', 'desc']],
      });
      const begin = lastSyncedTransaction
        ? moment(lastSyncedTransaction.createdAt).add(1, 'second').toISOString()
        : card.createdAt;
      logger.info(`Fetching transactions since ${begin}`);

      const { data: transactions } = await privacyLib.listTransactions(
        connectedAccount.token,
        card.id,
        {
          begin,
          // Assumption: We won't have more than 200 transactions out of sync.
          // eslint-disable-next-line camelcase
          page_size: 200,
        },
        'approvals',
      );
      const hostCurrencyFxRate = await getFxRate('USD', host.currency);

      if (DRY) {
        logger.info(`Found ${transactions.length} pending transactions...`);
        logger.debug(JSON.stringify(transactions, null, 2));
      } else {
        logger.info(`Syncing ${transactions.length} pending transactions...`);
        await Promise.all(
          transactions.map(transaction => privacy.createExpense(transaction, { host, hostCurrencyFxRate })),
        );
        if (host.settings?.virtualcards?.autopause) {
          await privacy.autoPauseResumeCard(card);
        }
      }
    }
  }
}

if (require.main === module) {
  run()
    .then(() => {
      process.exit(0);
    })
    .catch(e => {
      console.error(e);
      process.exit(1);
    });
}
<commit_msg>fix: worker date resolver<commit_after>#!/usr/bin/env node
import '../../server/env';

import moment from 'moment';

import { Service as ConnectedAccountServices } from '../../server/constants/connected_account';
import { getFxRate } from '../../server/lib/currency';
import logger from '../../server/lib/logger';
import * as privacyLib from '../../server/lib/privacy';
import models from '../../server/models';
import privacy from '../../server/paymentProviders/privacy';

const DRY = process.env.DRY;

export async function run() {
  logger.info('Reconciling Privacy.com Credit Card transactions...');
  if (DRY) {
    logger.warn(`Running DRY, no changes to the DB`);
  }

  const connectedAccounts = await models.ConnectedAccount.findAll({
    where: { service: ConnectedAccountServices.PRIVACY },
  });
  logger.info(`Found ${connectedAccounts.length} connected Privacy accounts...`);

  for (const connectedAccount of connectedAccounts) {
    const host = await models.Collective.findByPk(connectedAccount.CollectiveId);
    const cards = await models.VirtualCard.findAll({ where: { HostCollectiveId: host.id } });
    logger.info(`Found ${cards.length} cards connected to host #${connectedAccount.CollectiveId} ${host.slug}...`);

    for (const card of cards) {
      const lastSyncedTransaction = await models.Expense.findOne({
        where: { VirtualCardId: card.id },
        order: [['createdAt', 'desc']],
      });
      const begin = lastSyncedTransaction
        ? moment(lastSyncedTransaction.createdAt).add(1, 'second').toISOString()
        : moment(card.createdAt).toISOString();
      logger.info(`Fetching transactions since ${begin}`);

      const { data: transactions } = await privacyLib.listTransactions(
        connectedAccount.token,
        card.id,
        {
          begin,
          // Assumption: We won't have more than 200 transactions out of sync.
          // eslint-disable-next-line camelcase
          page_size: 200,
        },
        'approvals',
      );
      const hostCurrencyFxRate = await getFxRate('USD', host.currency);

      if (DRY) {
        logger.info(`Found ${transactions.length} pending transactions...`);
        logger.debug(JSON.stringify(transactions, null, 2));
      } else {
        logger.info(`Syncing ${transactions.length} pending transactions...`);
        await Promise.all(
          transactions.map(transaction => privacy.createExpense(transaction, { host, hostCurrencyFxRate })),
        );
        if (host.settings?.virtualcards?.autopause) {
          await privacy.autoPauseResumeCard(card);
        }
      }
    }
  }
}

if (require.main === module) {
  run()
    .then(() => {
      process.exit(0);
    })
    .catch(e => {
      console.error(e);
      process.exit(1);
    });
}
<|endoftext|>


<commit_before>/*globals $, svgCanvas, svgEditor*/
/*jslint regexp:true*/
svgEditor.addExtension("php_savefile", {
	callback: function() {
		'use strict';
		function getFileNameFromTitle () {
			var title = svgCanvas.getDocumentTitle();
			return $.trim(title); // .replace(/[^a-z0-9\.\_\-]+/gi, '_'); // We could do this more stringent client-side filtering, but we need to do on the server anyways
		}
		var save_svg_action = 'extensions/savefile.php';
		svgEditor.setCustomHandlers({
			save: function(win, data) {
				var svg = "<?xml version=\"1.0\"?>\n" + data,
					filename = getFileNameFromTitle();

				$.post(save_svg_action, {output_svg: svg, filename: filename});
			}
		});
	}
});

this.saveHandler = function(svg) {'use strict';
	$.post("svg-editor-save.php", {svg_data: svg});
};
<commit_msg>Possible todo to support server-side saving of exports as well<commit_after>/*globals $, svgCanvas, svgEditor*/
/*jslint regexp:true*/
// TODO: Might add support for "exportImage" custom
//   handler as in "ext-server_opensave.js" (and in savefile.php)

svgEditor.addExtension("php_savefile", {
	callback: function() {
		'use strict';
		function getFileNameFromTitle () {
			var title = svgCanvas.getDocumentTitle();
			return $.trim(title); // .replace(/[^a-z0-9\.\_\-]+/gi, '_'); // We could do this more stringent client-side filtering, but we need to do on the server anyways
		}
		var save_svg_action = 'extensions/savefile.php';
		svgEditor.setCustomHandlers({
			save: function(win, data) {
				var svg = "<?xml version=\"1.0\"?>\n" + data,
					filename = getFileNameFromTitle();

				$.post(save_svg_action, {output_svg: svg, filename: filename});
			}
		});
	}
});

this.saveHandler = function(svg) {'use strict';
	$.post("svg-editor-save.php", {svg_data: svg});
};
<|endoftext|>


<commit_before>import {font as cfg, core as coreCfg} from '../config';
import {fonts, alignment as alignmentConst} from '../const'
import {drawImage, cacheSprite, initCanvas, drawPixel} from '../util'

const sprite = initCanvas(coreCfg.pixelSize, coreCfg.pixelSize),
	fontProto = {
	meta: null,
	glyphs: null,
	write: function(ctx, coords, text, options = {}) {
		let color = options.color || cfg.defaultColor,
			size = options.size || coreCfg.pixelSize,
			alignment = options.alignment || cfg.alignment,
			[x, y] = coords;

		switch (alignment) {
			case alignmentConst.center:
				let lineWidth = 0;
				for (let i = 0; i < text.length; i += 1) {
					lineWidth += this.glyphs[text[i]].dWidth * size
				}
				x -= Math.floor(lineWidth / 2);
				break;
			case alignmentConst.right:
				for (let i = 0; i < text.length; i += 1) {
					x -= this.glyphs[text[i]].dWidth * size
				}
				break;
		}
		for (let i = 0; i < text.length; i += 1) {
			this.writeLetter(ctx, text[i], x, y, color, size);
			x += this.glyphs[text[i]].dWidth * size;
		}
	},
	writeLetter(ctx, letter, x, y, color, size) {
		const style = color + '-' + size;
		if (!this.glyphs[letter].sprites[style]) {
			this.drawLetter(this.glyphs[letter], color, size);
		}
		drawImage(ctx, this.glyphs[letter].sprites[style].ctx,
			[x + this.glyphs[letter].x * size, y + this.glyphs[letter].y * size],
			this.glyphs[letter].sprites[style].coords, [this.meta.boundingBox.width * size, this.meta.boundingBox.height * size]);
	},
	drawLetter(glyph, color, size) {
		if (sprite.canvas.width !== glyph.width * size) {
			sprite.canvas.width = glyph.width * size;
		}
		if (sprite.canvas.height !== glyph.height * size) {
			sprite.canvas.height = glyph.height * size;
		}
		sprite.clearRect(0, 0, glyph.width * size, glyph.height * size);
		for (let i = 0; i < glyph.bitmap.length; i += 1) {
			if (glyph.bitmap[i]) {
				drawPixel(sprite, i % glyph.width * size, Math.floor(i / glyph.width) * size, color, size)
			}
		}
		glyph.sprites[color + '-' + size] = cacheSprite(sprite);
	}
};

export function create(name) {
	const font = Object.create(fontProto);

	font.meta = fonts[name].meta;
	font.glyphs = Object.assign({}, fonts[name].glyphs);
	Object.keys(font.glyphs).forEach(key => {
		let width = font.glyphs[key].width || font.meta.boundingBox.width;
		font.glyphs[key].sprites = {};
		font.glyphs[key].bitmap = font.glyphs[key].bytes
			.map(val => val.toString(2))
			.map(val => '0'.repeat(width - val.length) + val)
			.join('').split('').map(val => parseInt(val, 10));

		font.glyphs[key].width = font.glyphs[key].width || font.meta.boundingBox.width;
		font.glyphs[key].height = font.glyphs[key].height || font.meta.boundingBox.height;
		font.glyphs[key].x = font.glyphs[key].x || font.meta.boundingBox.x;
		font.glyphs[key].y = font.glyphs[key].y || font.meta.boundingBox.y;
		font.glyphs[key].dWidth = font.glyphs[key].dWidth || font.meta.boundingBox.width;
	});

	return font;
}<commit_msg>Support for fonts with variable letter height<commit_after>import {font as cfg, core as coreCfg} from '../config';
import {fonts, alignment as alignmentConst} from '../const'
import {drawImage, cacheSprite, initCanvas, drawPixel} from '../util'

const sprite = initCanvas(coreCfg.pixelSize, coreCfg.pixelSize),
	fontProto = {
	meta: null,
	glyphs: null,
	write: function(ctx, coords, text, options = {}) {
		let color = options.color || cfg.defaultColor,
			size = options.size || coreCfg.pixelSize,
			alignment = options.alignment || cfg.alignment,
			[x, y] = coords;

		switch (alignment) {
			case alignmentConst.center:
				let lineWidth = 0;
				for (let i = 0; i < text.length; i += 1) {
					lineWidth += this.glyphs[text[i]].dWidth * size
				}
				x -= Math.floor(lineWidth / 2);
				break;
			case alignmentConst.right:
				for (let i = 0; i < text.length; i += 1) {
					x -= this.glyphs[text[i]].dWidth * size
				}
				break;
		}
		for (let i = 0; i < text.length; i += 1) {
			this.writeLetter(ctx, text[i], x, y, color, size);
			x += this.glyphs[text[i]].dWidth * size;
		}
	},
	writeLetter(ctx, letter, x, y, color, size) {
		const style = color + '-' + size;
		if (!this.glyphs[letter].sprites[style]) {
			this.drawLetter(this.glyphs[letter], color, size);
		}
		drawImage(ctx, this.glyphs[letter].sprites[style].ctx,
			[x + this.glyphs[letter].x * size, y - (this.glyphs[letter].y + this.glyphs[letter].height) * size],
			this.glyphs[letter].sprites[style].coords, [this.meta.boundingBox.width * size, this.meta.boundingBox.height * size]);
	},
	drawLetter(glyph, color, size) {
		if (sprite.canvas.width !== this.meta.boundingBox.width * size) {
			sprite.canvas.width = this.meta.boundingBox.width * size;
		}
		if (sprite.canvas.height !== this.meta.boundingBox.height * size) {
			sprite.canvas.height = this.meta.boundingBox.height * size;
		}
		sprite.clearRect(0, 0, this.meta.boundingBox.width * size, this.meta.boundingBox.height * size);
		for (let i = 0; i < glyph.height; i += 1) {
			for (let j = 0; j < glyph.height; j += 1) {
				if (glyph.bitmap[i] && glyph.bitmap[i][j]) {
					drawPixel(sprite, j * size, i * size, color, size);
				}
			}
		}
		glyph.sprites[color + '-' + size] = cacheSprite(sprite);
	}
};

export function create(name) {
	const font = Object.create(fontProto);

	font.meta = fonts[name].meta;
	font.glyphs = Object.assign({}, fonts[name].glyphs);
	Object.keys(font.glyphs).forEach(key => {
		let width = font.glyphs[key].width || font.meta.boundingBox.width;
		font.glyphs[key].sprites = {};
		font.glyphs[key].bitmap = [];

		for (let i = 0; i < font.glyphs[key].bytes.length; i+= 1) {
			font.glyphs[key].bitmap[i] = [];
			for (let bit = 0; bit < 8; bit += 1) {
				font.glyphs[key].bitmap[i][7 - bit] = (font.glyphs[key].bytes[i] >> bit) % 2;
			}
		}

		font.glyphs[key].width = 'width' in font.glyphs[key] ? font.glyphs[key].width : font.meta.boundingBox.width;
		font.glyphs[key].height = 'height' in font.glyphs[key] ? font.glyphs[key].height : font.meta.boundingBox.height;
		font.glyphs[key].x = 'x' in font.glyphs[key] ? font.glyphs[key].x : font.meta.boundingBox.x;
		font.glyphs[key].y = 'y' in font.glyphs[key] ? font.glyphs[key].y : font.meta.boundingBox.y;
		font.glyphs[key].dWidth = 'dWidth' in font.glyphs[key] ? font.glyphs[key].dWidth : font.meta.boundingBox.width;
	});

	return font;
}<|endoftext|>


<commit_before>import PropTypes from "prop-types";
import React from 'react';
import {
  InputGroup,
  Card,
  CardBody,
  CardHeader,
  Col,
  FormGroup,
  Input,
  Row,
  InputGroupAddon,
  Label
} from 'reactstrap';
import CopyButton from './CopyButton';

import { calculateUnits } from '../services/unitConverter';

class Converter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { etherConversions: []};
  }

  componentDidMount() {
    this.setState({etherConversions: calculateUnits(this.props.baseEther, 'Ether')});
  }

  handleOnChange(event, key) {
    const newUnits = calculateUnits(event.target.value, key);
    this.setState({etherConversions: newUnits});
    const newBaseEther = newUnits.find(unit => unit.key === 'ether');
    this.props.updateBaseEther(newBaseEther.value);
  }

  render() {
    return(
      <Row className="justify-content-md-center">
        <Col xs="12" sm="9" lg="9">
          <Card>
            <CardHeader>
              Ether Converter
            </CardHeader>
            <CardBody>
              {
                this.state.etherConversions.map(unit => (
                  <FormGroup key={unit.key}>
                    {unit.name}
                    <InputGroup>
                      <Input id={unit.name} placeholder={unit.name} value={unit.value} onChange={e => this.handleOnChange(e, unit.key)} />
                      <InputGroupAddon addonType="append">
                        <CopyButton text={unit.value} title="Copy value to clipboard" size={2}/>
                      </InputGroupAddon>
                    </InputGroup>
                  </FormGroup>
                ))
              }
            </CardBody>
          </Card>
        </Col>
      </Row>
    );
  }
}

Converter.propTypes = {
  baseEther: PropTypes.string,
  updateBaseEther: PropTypes.func
};

export default Converter;
<commit_msg>fix(@embark/cockpit/converter): allow decimal numbers<commit_after>import PropTypes from "prop-types";
import React from 'react';
import {
  InputGroup,
  Card,
  CardBody,
  CardHeader,
  Col,
  FormGroup,
  Input,
  Row,
  InputGroupAddon,
  Label
} from 'reactstrap';
import CopyButton from './CopyButton';

import { calculateUnits } from '../services/unitConverter';

class Converter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { etherConversions: []};
  }

  componentDidMount() {
    this.setState({etherConversions: calculateUnits(this.props.baseEther, 'Ether')});
  }

  handleOnChange(event, key) {
    const value = event.target.value;
    let newUnits;
    if (value.slice(-1) === '.') {
      // `calculateUnits()` turns `1.` to `1` which makes it impossible
      // for users to get beyond the first dot when typing decimal values.
      // That's why we bypass recalculation all together when the last character
      // is a dot and only update the form control in question.
      newUnits = this.state.etherConversions.map(unit => {
        if (unit.key === key) {
          unit.value = value;
        }
        return unit;
      });
      this.setState({etherConversions: newUnits});
    } else {
      newUnits = calculateUnits(value, key);
      this.setState({etherConversions: newUnits});
    }
    const newBaseEther = newUnits.find(unit => unit.key === 'ether');
    this.props.updateBaseEther(newBaseEther.value);
  }

  render() {
    return(
      <Row className="justify-content-md-center">
        <Col xs="12" sm="9" lg="9">
          <Card>
            <CardHeader>
              Ether Converter
            </CardHeader>
            <CardBody>
              {
                this.state.etherConversions.map(unit => (
                  <FormGroup key={unit.key}>
                    {unit.name}
                    <InputGroup>
                      <Input id={unit.name} placeholder={unit.name} value={unit.value} onChange={e => this.handleOnChange(e, unit.key)} />
                      <InputGroupAddon addonType="append">
                        <CopyButton text={unit.value} title="Copy value to clipboard" size={2}/>
                      </InputGroupAddon>
                    </InputGroup>
                  </FormGroup>
                ))
              }
            </CardBody>
          </Card>
        </Col>
      </Row>
    );
  }
}

Converter.propTypes = {
  baseEther: PropTypes.string,
  updateBaseEther: PropTypes.func
};

export default Converter;
<|endoftext|>


<commit_before>import expect from 'expect';
import isImmutableDefault from '../src/isImmutable';
import trackForMutations from '../src/trackForMutations';

describe('trackForMutations', () => {
  function testCasesForMutation(spec) {
    it('returns true and the mutated path', () => {
      const state = spec.getState();
      const options = spec.middlewareOptions || {};
      const { isImmutable = isImmutableDefault, ignore } = options;
      const tracker = trackForMutations(isImmutable, ignore, state);
      const newState = spec.fn(state);

      expect(
        tracker.detectMutations()
      ).toEqual({wasMutated: true, path: spec.path});
    });
  }

  function testCasesForNonMutation(spec) {
    it('returns false', () => {
      const state = spec.getState();
      const options = spec.middlewareOptions || {};
      const { isImmutable = isImmutableDefault, ignore } = options;
      const tracker = trackForMutations(isImmutable, ignore, state);
      const newState = spec.fn(state);

      expect(
        tracker.detectMutations()
      ).toEqual({wasMutated: false});
    });
  }

  const mutations = {
    'adding to nested array': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        s.foo.bar.push(5);
        return s;
      },
      path: ['foo', 'bar', '3']
    },
    'adding to nested array and setting new root object': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        s.foo.bar.push(5);
        return {...s};
      },
      path: ['foo', 'bar', '3']
    },
    'changing nested string': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        s.foo.baz = 'changed!';
        return s;
      },
      path: ['foo', 'baz']
    },
    'removing nested state': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        delete s.foo;
        return s;
      },
      path: ['foo']
    },
    'adding to array': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        s.stuff.push(1);
        return s;
      },
      path: ['stuff', '0']
    },
    'adding object to array': {
      getState: () => ({
        stuff: []
      }),
      fn: (s) => {
        s.stuff.push({foo: 1, bar: 2});
        return s;
      },
      path: ['stuff', '0']
    },
    'mutating previous state and returning new state': {
      getState: () => ({ counter: 0 }),
      fn: (s) => {
        s.mutation = true;
        return { ...s, counter: s.counter + 1 };
      },
      path: ['mutation']
    },
    'mutating previous state with non immutable type and returning new state': {
      getState: () => ({ counter: 0 }),
      fn: (s) => {
        s.mutation = [1, 2, 3];
        return { ...s, counter: s.counter + 1 };
      },
      path: ['mutation']
    },
    'mutating previous state with non immutable type and returning new state without that property': {
      getState: () => ({ counter: 0 }),
      fn: (s) => {
        s.mutation = [1, 2, 3];
        return { counter: s.counter + 1 };
      },
      path: ['mutation']
    },
    'mutating previous state with non immutable type and returning new simple state': {
      getState: () => ({ counter: 0 }),
      fn: (s) => {
        s.mutation = [1, 2, 3];
        return 1;
      },
      path: ['mutation']
    },
    'mutating previous state by deleting property and returning new state without that property': {
      getState: () => ({ counter: 0, toBeDeleted: true }),
      fn: (s) => {
        delete s.toBeDeleted;
        return { counter: s.counter + 1 };
      },
      path: ['toBeDeleted']
    },
    'mutating previous state by deleting nested property': {
      getState: () => ({ nested: { counter: 0, toBeDeleted: true }, foo: 1 }),
      fn: (s) => {
        delete s.nested.toBeDeleted;
        return { nested: { counter: s.counter + 1 } };
      },
      path: ['nested', 'toBeDeleted']
    },
    'update reference': {
      getState: () => ({ foo: {} }),
      fn: (s) => {
        s.foo = {};
        return s;
      },
      path: ['foo']
    },
    'catching state mutation in non-ignored branch': {
      getState: () => ({
        foo: {
          bar: [1, 2]
        },
        boo: {
          yah: [1, 2]
        }
      }),
      fn: (s) => {
        s.foo.bar.push(3);
        s.boo.yah.push(3);
        return s;
      },
      middlewareOptions: {
        ignore: ['foo']
      },
      path: ['boo', 'yah', '2']
    }
  };

  Object.keys(mutations).forEach((mutationDesc) => {
    describe(mutationDesc, () => {
      testCasesForMutation(mutations[mutationDesc]);
    });
  });

  const nonMutations = {
    'not doing anything': {
      getState: () => ({ a:1, b:2 }),
      fn: (s) => s
    },
    'from undefined to something': {
      getState: () => undefined,
      fn: (s) => ({foo: 'bar'})
    },
    'returning same state': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => s
    },
    'returning a new state object with nested new string': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        return {...s, foo: {...s.foo, baz: 'changed!'}};
      }
    },
    'returning a new state object with nested new array': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        return {...s, foo: {...s.foo, bar: [...s.foo.bar, 5]}};
      }
    },
    'removing nested state': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        return {...s, foo: {}};
      }
    },
    'having a NaN in the state': {
      getState: () => ({ a:NaN, b: Number.NaN }),
      fn: (s) => s
    },
    'ignoring branches from mutation detection': {
      getState: () => ({
        foo: {
          bar: 'bar'
        },
      }),
      fn: (s) => {
        s.foo.bar = 'baz'
        return s;
      },
      middlewareOptions: {
        ignore: ['foo']
      },
    },
    'ignoring nested branches from mutation detection': {
      getState: () => ({
        foo: {
          bar: [1, 2],
          boo: {
            yah: [1, 2]
          }
        },
      }),
      fn: (s) => {
        s.foo.bar.push(3);
        s.foo.boo.yah.push(3);
        return s;
      },
      middlewareOptions: {
        ignore: [
          'foo.bar',
          'foo.boo.yah',
        ]
      }
    },
    'ignoring nested array indices from mutation detection': {
      getState: () => ({
        stuff: [{a: 1}, {a: 2}]
      }),
      fn: (s) => {
        s.stuff[1].a = 3
        return s;
      },
      middlewareOptions: {
        ignore: ['stuff.1']
      }
    }
  };

  Object.keys(nonMutations).forEach((nonMutationDesc) => {
    describe(nonMutationDesc, () => {
      testCasesForNonMutation(nonMutations[nonMutationDesc]);
    });
  });
});
<commit_msg>added empty string test for ignore option<commit_after>import expect from 'expect';
import isImmutableDefault from '../src/isImmutable';
import trackForMutations from '../src/trackForMutations';

describe('trackForMutations', () => {
  function testCasesForMutation(spec) {
    it('returns true and the mutated path', () => {
      const state = spec.getState();
      const options = spec.middlewareOptions || {};
      const { isImmutable = isImmutableDefault, ignore } = options;
      const tracker = trackForMutations(isImmutable, ignore, state);
      const newState = spec.fn(state);

      expect(
        tracker.detectMutations()
      ).toEqual({wasMutated: true, path: spec.path});
    });
  }

  function testCasesForNonMutation(spec) {
    it('returns false', () => {
      const state = spec.getState();
      const options = spec.middlewareOptions || {};
      const { isImmutable = isImmutableDefault, ignore } = options;
      const tracker = trackForMutations(isImmutable, ignore, state);
      const newState = spec.fn(state);

      expect(
        tracker.detectMutations()
      ).toEqual({wasMutated: false});
    });
  }

  const mutations = {
    'adding to nested array': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        s.foo.bar.push(5);
        return s;
      },
      path: ['foo', 'bar', '3']
    },
    'adding to nested array and setting new root object': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        s.foo.bar.push(5);
        return {...s};
      },
      path: ['foo', 'bar', '3']
    },
    'changing nested string': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        s.foo.baz = 'changed!';
        return s;
      },
      path: ['foo', 'baz']
    },
    'removing nested state': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        delete s.foo;
        return s;
      },
      path: ['foo']
    },
    'adding to array': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        s.stuff.push(1);
        return s;
      },
      path: ['stuff', '0']
    },
    'adding object to array': {
      getState: () => ({
        stuff: []
      }),
      fn: (s) => {
        s.stuff.push({foo: 1, bar: 2});
        return s;
      },
      path: ['stuff', '0']
    },
    'mutating previous state and returning new state': {
      getState: () => ({ counter: 0 }),
      fn: (s) => {
        s.mutation = true;
        return { ...s, counter: s.counter + 1 };
      },
      path: ['mutation']
    },
    'mutating previous state with non immutable type and returning new state': {
      getState: () => ({ counter: 0 }),
      fn: (s) => {
        s.mutation = [1, 2, 3];
        return { ...s, counter: s.counter + 1 };
      },
      path: ['mutation']
    },
    'mutating previous state with non immutable type and returning new state without that property': {
      getState: () => ({ counter: 0 }),
      fn: (s) => {
        s.mutation = [1, 2, 3];
        return { counter: s.counter + 1 };
      },
      path: ['mutation']
    },
    'mutating previous state with non immutable type and returning new simple state': {
      getState: () => ({ counter: 0 }),
      fn: (s) => {
        s.mutation = [1, 2, 3];
        return 1;
      },
      path: ['mutation']
    },
    'mutating previous state by deleting property and returning new state without that property': {
      getState: () => ({ counter: 0, toBeDeleted: true }),
      fn: (s) => {
        delete s.toBeDeleted;
        return { counter: s.counter + 1 };
      },
      path: ['toBeDeleted']
    },
    'mutating previous state by deleting nested property': {
      getState: () => ({ nested: { counter: 0, toBeDeleted: true }, foo: 1 }),
      fn: (s) => {
        delete s.nested.toBeDeleted;
        return { nested: { counter: s.counter + 1 } };
      },
      path: ['nested', 'toBeDeleted']
    },
    'update reference': {
      getState: () => ({ foo: {} }),
      fn: (s) => {
        s.foo = {};
        return s;
      },
      path: ['foo']
    },
    'cannot ignore root state': {
      getState: () => ({ foo: {} }),
      fn: (s) => {
        s.foo = {};
        return s;
      },
      middlewareOptions: {
        ignore: ['']
      },
      path: ['foo']
    },
    'catching state mutation in non-ignored branch': {
      getState: () => ({
        foo: {
          bar: [1, 2]
        },
        boo: {
          yah: [1, 2]
        }
      }),
      fn: (s) => {
        s.foo.bar.push(3);
        s.boo.yah.push(3);
        return s;
      },
      middlewareOptions: {
        ignore: ['foo']
      },
      path: ['boo', 'yah', '2']
    }
  };

  Object.keys(mutations).forEach((mutationDesc) => {
    describe(mutationDesc, () => {
      testCasesForMutation(mutations[mutationDesc]);
    });
  });

  const nonMutations = {
    'not doing anything': {
      getState: () => ({ a:1, b:2 }),
      fn: (s) => s
    },
    'from undefined to something': {
      getState: () => undefined,
      fn: (s) => ({foo: 'bar'})
    },
    'returning same state': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => s
    },
    'returning a new state object with nested new string': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        return {...s, foo: {...s.foo, baz: 'changed!'}};
      }
    },
    'returning a new state object with nested new array': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        return {...s, foo: {...s.foo, bar: [...s.foo.bar, 5]}};
      }
    },
    'removing nested state': {
      getState: () => ({
        foo: {
          bar: [2, 3, 4],
          baz: 'baz'
        },
        stuff: []
      }),
      fn: (s) => {
        return {...s, foo: {}};
      }
    },
    'having a NaN in the state': {
      getState: () => ({ a:NaN, b: Number.NaN }),
      fn: (s) => s
    },
    'ignoring branches from mutation detection': {
      getState: () => ({
        foo: {
          bar: 'bar'
        },
      }),
      fn: (s) => {
        s.foo.bar = 'baz'
        return s;
      },
      middlewareOptions: {
        ignore: ['foo']
      },
    },
    'ignoring nested branches from mutation detection': {
      getState: () => ({
        foo: {
          bar: [1, 2],
          boo: {
            yah: [1, 2]
          }
        },
      }),
      fn: (s) => {
        s.foo.bar.push(3);
        s.foo.boo.yah.push(3);
        return s;
      },
      middlewareOptions: {
        ignore: [
          'foo.bar',
          'foo.boo.yah',
        ]
      }
    },
    'ignoring nested array indices from mutation detection': {
      getState: () => ({
        stuff: [{a: 1}, {a: 2}]
      }),
      fn: (s) => {
        s.stuff[1].a = 3
        return s;
      },
      middlewareOptions: {
        ignore: ['stuff.1']
      }
    }
  };

  Object.keys(nonMutations).forEach((nonMutationDesc) => {
    describe(nonMutationDesc, () => {
      testCasesForNonMutation(nonMutations[nonMutationDesc]);
    });
  });
});
<|endoftext|>