<commit_before>import { get, set, computed, setProperties } from '@ember/object';
import { resolve } from 'rsvp';
import { scheduleOnce } from '@ember/runloop';
import { alias, notEmpty } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import NewOrEdit from 'shared/mixins/new-or-edit';
import C from 'ui/utils/constants';
import Util from 'ui/utils/util';
import { task } from 'ember-concurrency';
import YAML from 'yamljs';
import layout from './template';
import { isEmpty } from '@ember/utils';
import CatalogApp from 'shared/mixins/catalog-app';

export default Component.extend(NewOrEdit, CatalogApp, {
  catalog:                  service(),
  intl:                     service(),
  scope:                    service(),
  router:                   service(),
  settings:                 service(),
  globalStore:              service(),

  layout,
  namespaceErrors:          null,
  allTemplates:             null,
  templateResource:         null,
  namespaceResource:        null,
  versionsArray:            null,
  versionsLinks:            null,
  namespaces:               null,
  actuallySave:             true,
  showHeader:               true,
  showPreview:              true,
  customizeNamespace:       false,
  decoding:                 false,
  forceUpgrade:             false,
  titleAdd:                 'newCatalog.titleAdd',
  titleUpgrade:             'newCatalog.titleUpgrade',
  selectVersionAdd:         'newCatalog.selectVersionAdd',
  selectVersionUpgrade:     'newCatalog.selectVersionUpgrade',
  saveUpgrade:              'newCatalog.saveUpgrade',
  saveNew:                  'newCatalog.saveNew',
  sectionClass:             'box mb-20',
  showDefaultVersionOption: false,

  classNames:               ['launch-catalog'],
  catalogApp:               null,
  srcSet:                   false,

  detailExpanded:           false,
  previewOpen:              false,
  previewTab:               null,
  questionsArray:           null,
  selectedTemplateUrl:      null,
  selectedTemplateModel:    null,
  readmeContent:            null,
  appReadmeContent:         null,
  pastedAnswers:            null,
  noAppReadme:              null,
  selectedFileContetnt:     null,

  isGKE:                    alias('scope.currentCluster.isGKE'),

  primaryResource:          alias('namespaceResource'),
  editing:                  notEmpty('catalogApp.id'),
  requiredNamespace: alias('selectedTemplateModel.requiredNamespace'),

  init() {
    this._super(...arguments);
    set(this, 'selectedTemplateModel', null);

    scheduleOnce('afterRender', () => {
      if ( get(this, 'selectedTemplateUrl') ) {
        this.templateChanged();
      } else {
        var def = get(this, 'templateResource.defaultVersion');
        var links = get(this, 'versionLinks');
        var app = get(this, 'catalogApp');

        if (get(app, 'id') && !get(this, 'upgrade')) {
          def = get(app, 'externalIdInfo.version');
        }

        if (links[def]) {
          set(this, 'selectedTemplateUrl', links[def]);
        } else {
          set(this, 'selectedTemplateUrl', null);
        }
      }
    });
  },

  didRender() {
    if (!this.get('srcSet')) {
      this.set('srcSet', true);

      const $icon = this.$('img');

      $icon.attr('src', $icon.data('src'));
      this.$('img').on('error', () => {
        $icon.attr('src', `${ this.get('app.baseAssets') }assets/images/generic-catalog.svg`);
      });
    }
  },

  actions: {
    toogleDetailedDescriptions() {
      set(this, 'detailExpanded', true);
    },

    toogleNamespace() {
      set(this, 'customizeNamespace', true);
    },

    cancel() {
      this.sendAction('cancel');
    },

    togglePreview() {
      this.toggleProperty('previewOpen');
    },

    selectPreviewTab(tab) {
      set(this, 'previewTab', tab);
    },
  },

  answersArray: computed('selectedTemplateModel.questions', 'selectedTemplateModel.customAnswers', 'catalogApp.answers', function() {
    let model = get(this, 'selectedTemplateModel');

    if (get(model, 'questions')) {
      const questions = [];

      (get(this, 'selectedTemplateModel.questions') || []).forEach((q) => {
        questions.push(q);
        const subquestions = get(q, 'subquestions');

        if ( subquestions ) {
          questions.pushObjects(subquestions);
        }
      });

      const customAnswers = get(this, 'selectedTemplateModel.customAnswers') || {};

      Object.keys(customAnswers).forEach((key) => {
        questions.push({
          variable: key,
          answer:   customAnswers[key],
        });
      });

      return questions;
    } else {
      return get(this, 'catalogApp.answers');
    }
  }),

  answersString: computed('answersArray.@each.{variable,answer}', function() {
    let model = get(this, 'selectedTemplateModel');

    if (get(model, 'questions')) {
      let neu = {};

      (get(this, 'answersArray') || []).forEach((a) => {
        neu[a.variable] = isEmpty(a.answer) ? a.default : a.answer;
      });

      const customAnswers = get(model, 'customAnswers') || {};

      Object.keys(customAnswers).forEach((key) => {
        neu[key] = customAnswers[key];
      });

      return YAML.stringify(neu);
    } else {
      return JSON.stringify(get(this, 'answersArray'));
    }
  }),

  getTemplate: task(function * () {
    var url = get(this, 'selectedTemplateUrl');

    if ( url === 'default' ) {
      let defaultUrl = get(this, 'defaultUrl');

      if ( defaultUrl ) {
        url = defaultUrl;
      } else {
        url = null;
      }
    }

    if (url) {
      var version = get(this, 'settings.rancherVersion');

      if ( version ) {
        url = Util.addQueryParam(url, 'rancherVersion', version);
      }

      var current = get(this, 'catalogApp.answers');

      if ( !current ) {
        current = {};
        set(this, 'catalogApp.answers', current);
      }

      var selectedTemplateModel = yield get(this, 'catalog').fetchByUrl(url)
        .then((response) => {
          if (response.questions) {
            const questions = [];
            const customAnswers = {};

            response.questions.forEach((q) => {
              questions.push(q);
              const subquestions = get(q, 'subquestions');

              if ( subquestions ) {
                questions.pushObjects(subquestions);
              }
            });
            questions.forEach((item) => {
            // This will be the component that is rendered to edit this answer
              item.inputComponent = `schema/input-${ item.type }`;

              // Only types marked supported will show the component, Ember will explode if the component doesn't exist
              item.supported = C.SUPPORTED_SCHEMA_INPUTS.indexOf(item.type) >= 0;

              if (typeof current[item.variable] !== 'undefined') {
              // If there's an existing value, use it (for upgrade)
                item.answer = current[item.variable];
              } else if (item.type === 'service' || item.type === 'certificate') {
              // Loaded async and then the component picks the default
              } else if ( item.type === 'boolean' ) {
              // Coerce booleans
                item.answer = (item.default === 'true' || item.default === true);
              } else {
              // Everything else
                item.answer = item.default || null;
              }
            });

            Object.keys(current).forEach((key) => {
              const q = questions.findBy('variable', key);

              if ( !q ) {
                customAnswers[key] = current[key];
              }
            });

            response.customAnswers = customAnswers;
          }

          return response;
        });

      set(this, 'selectedTemplateModel', selectedTemplateModel);

      const files = Object.keys(selectedTemplateModel.get('files')) || [];

      if ( files.length > 0 ) {
        const valuesYaml = files.find((file) => file.endsWith('/values.yaml'));

        set(this, 'previewTab', valuesYaml ? valuesYaml : files[0]);
      }
    } else {
      set(this, 'selectedTemplateModel', null);
      set(this, 'readmeContent', null);
      set(this, 'appReadmeContent', null);
      set(this, 'noAppReadme', false);
    }

    this.updateReadme();
  }),

  validate() {
    this._super();

    const errors = get(this, 'errors') || [];

    errors.pushObjects(get(this, 'namespaceErrors') || []);
    errors.pushObjects(get(this, 'selectedTemplateModel').validationErrors() || []);

    if (errors.length) {
      set(this, 'errors', errors.uniq());

      return false;
    }

    return true;
  },

  doSave() {
    const requiredNamespace = get(this, 'requiredNamespace');

    if ( requiredNamespace && (get(this, 'namespaces') || []).findBy('id', requiredNamespace) ) {
      return resolve(get(this, 'primaryResource'));
    } else if ( requiredNamespace ) {
      set(this, 'primaryResource.name', requiredNamespace);
    }

    return this._super(...arguments);
  },

  willSave() {
    set(this, 'errors', null);
    var ok = this.validate();

    if (!ok) {
      // Validation failed
      return false;
    }
    if ( get(this, 'actuallySave') ) {
      if (get(this, 'selectedTemplateModel.questions')) {
        set(get(this, 'catalogApp'), 'answers', get(this, 'answers'));
      }

      return true;
    } else {
      // TODO 2.0 this is part of the volumes stuff so we need to investigate if this still works
      // let versionId = null;
      // if ( get(this, 'selectedTemplateUrl') !== 'default' && get(this, 'selectedTemplateModel') ) {
      //   versionId = get(this, 'selectedTemplateModel.id');
      // }

      // this.sendAction('doSave', {
      //   answers:           get(this, 'answers'),
      //   externalId:        get(this, 'newExternalId'),
      //   templateId:        get(this, 'templateResource.id'),
      //   templateVersionId: versionId,
      // });
      return false;
    }
  },

  didSave(neu) {
    let app = get(this, 'catalogApp');
    const yaml = get(this, 'selectedTemplateModel.valuesYaml');

    if (get(app, 'id')) {
      return app.doAction('upgrade', {
        externalId:   get(this, 'selectedTemplateModel.externalId'),
        answers:      yaml ? {} : get(app, 'answers'),
        valuesYaml:   yaml ? yaml : null,
        forceUpgrade: get(this, 'forceUpgrade'),
      }).then((resp) => resp)
        .catch((err) => err);
    } else {
      const requiredNamespace = get(this, 'requiredNamespace');

      setProperties(app, {
        targetNamespace: requiredNamespace ? requiredNamespace : neu.name,
        externalId:      get(this, 'selectedTemplateModel.externalId'),
        projectId:       get(neu, 'projectId'),
        answers:         yaml ? {} : get(app, 'answers'),
        valuesYaml:      yaml ? yaml : null,
      });

      return app.save().then(() => get(this, 'primaryResource'));
    }
  },

  doneSaving() {
    var projectId = get(this, 'scope.currentProject.id');

    return get(this, 'router').transitionTo('apps-tab.index', projectId);
  }
});
<commit_msg>Fall back to yaml if there is a numeric password in catalog answers<commit_after>import { get, set, computed, setProperties } from '@ember/object';
import { resolve } from 'rsvp';
import { scheduleOnce } from '@ember/runloop';
import { alias, notEmpty } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import NewOrEdit from 'shared/mixins/new-or-edit';
import C from 'ui/utils/constants';
import Util from 'ui/utils/util';
import { task } from 'ember-concurrency';
import YAML from 'yamljs';
import layout from './template';
import { isEmpty } from '@ember/utils';
import CatalogApp from 'shared/mixins/catalog-app';
import { isNumeric } from 'shared/utils/util';
import convertDotAnswersToYaml from 'shared/utils/convert-yaml';

export default Component.extend(NewOrEdit, CatalogApp, {
  catalog:                  service(),
  intl:                     service(),
  scope:                    service(),
  router:                   service(),
  settings:                 service(),
  globalStore:              service(),

  layout,
  namespaceErrors:          null,
  allTemplates:             null,
  templateResource:         null,
  namespaceResource:        null,
  versionsArray:            null,
  versionsLinks:            null,
  namespaces:               null,
  actuallySave:             true,
  showHeader:               true,
  showPreview:              true,
  customizeNamespace:       false,
  decoding:                 false,
  forceUpgrade:             false,
  titleAdd:                 'newCatalog.titleAdd',
  titleUpgrade:             'newCatalog.titleUpgrade',
  selectVersionAdd:         'newCatalog.selectVersionAdd',
  selectVersionUpgrade:     'newCatalog.selectVersionUpgrade',
  saveUpgrade:              'newCatalog.saveUpgrade',
  saveNew:                  'newCatalog.saveNew',
  sectionClass:             'box mb-20',
  showDefaultVersionOption: false,

  classNames:               ['launch-catalog'],
  catalogApp:               null,
  srcSet:                   false,

  detailExpanded:           false,
  previewOpen:              false,
  previewTab:               null,
  questionsArray:           null,
  selectedTemplateUrl:      null,
  selectedTemplateModel:    null,
  readmeContent:            null,
  appReadmeContent:         null,
  pastedAnswers:            null,
  noAppReadme:              null,
  selectedFileContetnt:     null,

  isGKE:                    alias('scope.currentCluster.isGKE'),

  primaryResource:          alias('namespaceResource'),
  editing:                  notEmpty('catalogApp.id'),
  requiredNamespace: alias('selectedTemplateModel.requiredNamespace'),

  init() {
    this._super(...arguments);
    set(this, 'selectedTemplateModel', null);

    scheduleOnce('afterRender', () => {
      if ( get(this, 'selectedTemplateUrl') ) {
        this.templateChanged();
      } else {
        var def = get(this, 'templateResource.defaultVersion');
        var links = get(this, 'versionLinks');
        var app = get(this, 'catalogApp');

        if (get(app, 'id') && !get(this, 'upgrade')) {
          def = get(app, 'externalIdInfo.version');
        }

        if (links[def]) {
          set(this, 'selectedTemplateUrl', links[def]);
        } else {
          set(this, 'selectedTemplateUrl', null);
        }
      }
    });
  },

  didRender() {
    if (!this.get('srcSet')) {
      this.set('srcSet', true);

      const $icon = this.$('img');

      $icon.attr('src', $icon.data('src'));
      this.$('img').on('error', () => {
        $icon.attr('src', `${ this.get('app.baseAssets') }assets/images/generic-catalog.svg`);
      });
    }
  },

  actions: {
    toogleDetailedDescriptions() {
      set(this, 'detailExpanded', true);
    },

    toogleNamespace() {
      set(this, 'customizeNamespace', true);
    },

    cancel() {
      this.sendAction('cancel');
    },

    togglePreview() {
      this.toggleProperty('previewOpen');
    },

    selectPreviewTab(tab) {
      set(this, 'previewTab', tab);
    },
  },

  answersArray: computed('selectedTemplateModel.questions', 'selectedTemplateModel.customAnswers', 'catalogApp.answers', function() {
    let model = get(this, 'selectedTemplateModel');

    if (get(model, 'questions')) {
      const questions = [];

      (get(this, 'selectedTemplateModel.questions') || []).forEach((q) => {
        questions.push(q);
        const subquestions = get(q, 'subquestions');

        if ( subquestions ) {
          questions.pushObjects(subquestions);
        }
      });

      const customAnswers = get(this, 'selectedTemplateModel.customAnswers') || {};

      Object.keys(customAnswers).forEach((key) => {
        questions.push({
          variable: key,
          answer:   customAnswers[key],
        });
      });

      return questions;
    } else {
      return get(this, 'catalogApp.answers');
    }
  }),

  answersString: computed('answersArray.@each.{variable,answer}', function() {
    let model = get(this, 'selectedTemplateModel');

    if (get(model, 'questions')) {
      let neu = {};

      (get(this, 'answersArray') || []).forEach((a) => {
        neu[a.variable] = isEmpty(a.answer) ? a.default : a.answer;
      });

      const customAnswers = get(model, 'customAnswers') || {};

      Object.keys(customAnswers).forEach((key) => {
        neu[key] = customAnswers[key];
      });

      return YAML.stringify(neu);
    } else {
      return JSON.stringify(get(this, 'answersArray'));
    }
  }),

  getTemplate: task(function * () {
    var url = get(this, 'selectedTemplateUrl');

    if ( url === 'default' ) {
      let defaultUrl = get(this, 'defaultUrl');

      if ( defaultUrl ) {
        url = defaultUrl;
      } else {
        url = null;
      }
    }

    if (url) {
      var version = get(this, 'settings.rancherVersion');

      if ( version ) {
        url = Util.addQueryParam(url, 'rancherVersion', version);
      }

      var current = get(this, 'catalogApp.answers');

      if ( !current ) {
        current = {};
        set(this, 'catalogApp.answers', current);
      }

      var selectedTemplateModel = yield get(this, 'catalog').fetchByUrl(url)
        .then((response) => {
          if (response.questions) {
            const questions = [];
            const customAnswers = {};

            response.questions.forEach((q) => {
              questions.push(q);
              const subquestions = get(q, 'subquestions');

              if ( subquestions ) {
                questions.pushObjects(subquestions);
              }
            });
            questions.forEach((item) => {
            // This will be the component that is rendered to edit this answer
              item.inputComponent = `schema/input-${ item.type }`;

              // Only types marked supported will show the component, Ember will explode if the component doesn't exist
              item.supported = C.SUPPORTED_SCHEMA_INPUTS.indexOf(item.type) >= 0;

              if (typeof current[item.variable] !== 'undefined') {
              // If there's an existing value, use it (for upgrade)
                item.answer = current[item.variable];
              } else if (item.type === 'service' || item.type === 'certificate') {
              // Loaded async and then the component picks the default
              } else if ( item.type === 'boolean' ) {
              // Coerce booleans
                item.answer = (item.default === 'true' || item.default === true);
              } else {
              // Everything else
                item.answer = item.default || null;
              }
            });

            Object.keys(current).forEach((key) => {
              const q = questions.findBy('variable', key);

              if ( !q ) {
                customAnswers[key] = current[key];
              }
            });

            response.customAnswers = customAnswers;
          }

          return response;
        });

      set(this, 'selectedTemplateModel', selectedTemplateModel);

      const files = Object.keys(selectedTemplateModel.get('files')) || [];

      if ( files.length > 0 ) {
        const valuesYaml = files.find((file) => file.endsWith('/values.yaml'));

        set(this, 'previewTab', valuesYaml ? valuesYaml : files[0]);
      }
    } else {
      set(this, 'selectedTemplateModel', null);
      set(this, 'readmeContent', null);
      set(this, 'appReadmeContent', null);
      set(this, 'noAppReadme', false);
    }

    this.updateReadme();
  }),

  validate() {
    this._super();

    const errors = get(this, 'errors') || [];

    errors.pushObjects(get(this, 'namespaceErrors') || []);
    errors.pushObjects(get(this, 'selectedTemplateModel').validationErrors() || []);

    if (errors.length) {
      set(this, 'errors', errors.uniq());

      return false;
    }

    return true;
  },

  doSave() {
    const requiredNamespace = get(this, 'requiredNamespace');

    if ( requiredNamespace && (get(this, 'namespaces') || []).findBy('id', requiredNamespace) ) {
      return resolve(get(this, 'primaryResource'));
    } else if ( requiredNamespace ) {
      set(this, 'primaryResource.name', requiredNamespace);
    }

    return this._super(...arguments);
  },

  willSave() {
    set(this, 'errors', null);
    var ok = this.validate();

    if (!ok) {
      // Validation failed
      return false;
    }
    if ( get(this, 'actuallySave') ) {
      if (get(this, 'selectedTemplateModel.questions')) {
        set(get(this, 'catalogApp'), 'answers', get(this, 'answers'));
      }

      return true;
    } else {
      // TODO 2.0 this is part of the volumes stuff so we need to investigate if this still works
      // let versionId = null;
      // if ( get(this, 'selectedTemplateUrl') !== 'default' && get(this, 'selectedTemplateModel') ) {
      //   versionId = get(this, 'selectedTemplateModel.id');
      // }

      // this.sendAction('doSave', {
      //   answers:           get(this, 'answers'),
      //   externalId:        get(this, 'newExternalId'),
      //   templateId:        get(this, 'templateResource.id'),
      //   templateVersionId: versionId,
      // });
      return false;
    }
  },

  didSave(neu) {
    let app = get(this, 'catalogApp');
    let yaml = get(this, 'selectedTemplateModel.valuesYaml');

    if ( !yaml && this.shouldFallBackToYaml() ) {
      yaml = convertDotAnswersToYaml(get(app, 'answers'));
    }

    if (get(app, 'id')) {
      return app.doAction('upgrade', {
        externalId:   get(this, 'selectedTemplateModel.externalId'),
        answers:      yaml ? {} : get(app, 'answers'),
        valuesYaml:   yaml ? yaml : null,
        forceUpgrade: get(this, 'forceUpgrade'),
      }).then((resp) => resp)
        .catch((err) => err);
    } else {
      const requiredNamespace = get(this, 'requiredNamespace');

      setProperties(app, {
        targetNamespace: requiredNamespace ? requiredNamespace : neu.name,
        externalId:      get(this, 'selectedTemplateModel.externalId'),
        projectId:       get(neu, 'projectId'),
        answers:         yaml ? {} : get(app, 'answers'),
        valuesYaml:      yaml ? yaml : null,
      });

      return app.save().then(() => get(this, 'primaryResource'));
    }
  },

  doneSaving() {
    var projectId = get(this, 'scope.currentProject.id');

    return get(this, 'router').transitionTo('apps-tab.index', projectId);
  },

  shouldFallBackToYaml() {
    const questions = get(this, 'selectedTemplateModel.allQuestions') || [];

    return !!questions.some((question) => get(question, 'type') === 'password' && !!isNumeric(get(question, 'answer')));
  },
});
<|endoftext|>


<commit_before>/**
 * Service to display status in all app views
 */
angular.module('lcaApp.status.service', ['angularSpinner', 'ui.bootstrap.alert'])
    .constant("SPINNER_KEY", "spinner-lca")
    .factory('StatusService', [ 'usSpinnerService', 'SPINNER_KEY', '$rootScope',
        function (usSpinnerService, SPINNER_KEY, $rootScope) {
            var svc = { };

            svc.startWaiting = function () {
                $rootScope.alert = null;
                usSpinnerService.spin(SPINNER_KEY);
            };

            svc.stopWaiting = function () {
                usSpinnerService.stop(SPINNER_KEY);
            };

            /**
             * Stop waiting and display error.
             * @param {string | {}} err     httpResponse for failed request, error message, or instance of Error
             */
            svc.handleFailure = function (err) {
                var errMsg = "";
                svc.stopWaiting();
                if (typeof(err) === "string") {
                    errMsg = err;
                } else {
                    if (err.hasOwnProperty("status")) {
                        if (err["status"] == 409) {
                            errMsg = "Web API request conflicts with another request that is in progress.\n";
                        }
                        if (err.hasOwnProperty("data")) {
                            if (typeof (err["data"])  === "string") {
                                errMsg += err["data"];
                            }
                            else {
                                var errData = err["data"];
                                if (errData.hasOwnProperty("exceptionMessage")) {
                                    errMsg = 'Web API Exception : "' + errData["exceptionMessage"] +
                                    '" See console for details.';
                                }
                            }
                        }
                    } else if (err.hasOwnProperty("message") ) {
                        errMsg = err["message"];
                    }

                }
                $rootScope.alert = { type: "danger", msg: errMsg };
            };

            svc.handleSuccess = function (infoMsg) {
                    svc.stopWaiting();
                    $rootScope.alert = null;
                    if (infoMsg) {
                        $rootScope.alert = { type: "success", msg: infoMsg };
                    }
            };

            return svc;
    }]);
<commit_msg>Vis: Dump web api response to console log when it contains an exception. (fixes #200)<commit_after>/**
 * Service to display status in all app views
 */
angular.module('lcaApp.status.service', ['angularSpinner', 'ui.bootstrap.alert'])
    .constant("SPINNER_KEY", "spinner-lca")
    .factory('StatusService', [ 'usSpinnerService', 'SPINNER_KEY', '$rootScope', '$log',
        function (usSpinnerService, SPINNER_KEY, $rootScope, $log) {
            var svc = { };

            svc.startWaiting = function () {
                $rootScope.alert = null;
                usSpinnerService.spin(SPINNER_KEY);
            };

            svc.stopWaiting = function () {
                usSpinnerService.stop(SPINNER_KEY);
            };

            /**
             * Stop waiting and display error.
             * @param {string | {}} err     httpResponse for failed request, error message, or instance of Error
             */
            svc.handleFailure = function (err) {
                var errMsg = "";
                svc.stopWaiting();
                if (typeof(err) === "string") {
                    errMsg = err;
                } else {
                    if (err.hasOwnProperty("status")) {
                        if (err["status"] == 409) {
                            errMsg = "Web API request conflicts with another request that is in progress.\n";
                        }
                        if (err.hasOwnProperty("data")) {
                            if (typeof (err["data"])  === "string") {
                                errMsg += err["data"];
                            }
                            else {
                                var errData = err["data"];
                                if (errData.hasOwnProperty("exceptionMessage")) {
                                    errMsg = 'Web API Exception : ' + errData["exceptionMessage"];
                                    $log.error(JSON.stringify(err));
                                }
                            }
                        }
                    } else if (err.hasOwnProperty("message") ) {
                        errMsg = err["message"];
                    }

                }
                $rootScope.alert = { type: "danger", msg: errMsg };
            };

            svc.handleSuccess = function (infoMsg) {
                    svc.stopWaiting();
                    $rootScope.alert = null;
                    if (infoMsg) {
                        $rootScope.alert = { type: "success", msg: infoMsg };
                    }
            };

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


<commit_before>const jsonrpc = {};

jsonrpc.call = function(
  connectionString,
  method,
  params,
  callback,
  errorCallback,
  connectFailedCallback,
  timeout
) {
  function checkAndParse(response) {
    if (response.status >= 200 && response.status < 300) {
      return response.json();
    } else {
      return response.json().then(json => {
        let error;
        if (json.error) {
          error = new Error(json.error);
        } else {
          error = new Error("Protocol error with unknown response signature");
        }
        return Promise.reject(error);
      }).catch(e => {
        console.log(e);
      });
    }
  }

  function makeRequest(url, options) {
    return new Promise((resolve, reject) => {
      fetch(url, options)
        .then(resolve)
        .catch(reject);

      if (timeout) {
        const e = new Error(__("Protocol request timed out"));
        setTimeout(() => {
          return reject(e);
        }, timeout);
      }
    });
  }

  const counter = parseInt(sessionStorage.getItem("JSONRPCCounter") || 0);
  const url = connectionString;
  const options = {
    method: "POST",
    body: JSON.stringify({
      jsonrpc: "2.0",
      method: method,
      params: params,
      id: counter,
    }),
  };

  sessionStorage.setItem("JSONRPCCounter", counter + 1);

  return fetch(url, options)
    .then(checkAndParse)
    .then(response => {
      const error =
        response.error || (response.result && response.result.error);

      if (!error && typeof callback === "function") {
        return callback(response.result);
      }

      if (error && typeof errorCallback === "function") {
        return errorCallback(error);
      }

      var errorEvent = new CustomEvent("unhandledError", {
        detail: {
          connectionString: connectionString,
          method: method,
          params: params,
          code: error.code,
          message: error.message || error,
          data: error.data,
        },
      });
      document.dispatchEvent(errorEvent);
    })
    .catch(e => {
      if (connectFailedCallback) {
        return connectFailedCallback(e);
      }

      var errorEvent = new CustomEvent("unhandledError", {
        detail: {
          connectionString: connectionString,
          method: method,
          params: params,
          code: e.response && e.response.status,
          message: __("Connection to API server failed"),
        },
      });
      document.dispatchEvent(errorEvent);
    });
};

export default jsonrpc;
<commit_msg>Convert console log message into console error message<commit_after>const jsonrpc = {};

jsonrpc.call = function(
  connectionString,
  method,
  params,
  callback,
  errorCallback,
  connectFailedCallback,
  timeout
) {
  function checkAndParse(response) {
    if (response.status >= 200 && response.status < 300) {
      return response.json();
    } else {
      return response.json().then(json => {
        let error;
        if (json.error) {
          error = new Error(json.error);
        } else {
          error = new Error("Protocol error with unknown response signature");
        }
        return Promise.reject(error);
      }).catch(e => {
        console.error(e);
      });
    }
  }

  function makeRequest(url, options) {
    return new Promise((resolve, reject) => {
      fetch(url, options)
        .then(resolve)
        .catch(reject);

      if (timeout) {
        const e = new Error(__("Protocol request timed out"));
        setTimeout(() => {
          return reject(e);
        }, timeout);
      }
    });
  }

  const counter = parseInt(sessionStorage.getItem("JSONRPCCounter") || 0);
  const url = connectionString;
  const options = {
    method: "POST",
    body: JSON.stringify({
      jsonrpc: "2.0",
      method: method,
      params: params,
      id: counter,
    }),
  };

  sessionStorage.setItem("JSONRPCCounter", counter + 1);

  return fetch(url, options)
    .then(checkAndParse)
    .then(response => {
      const error =
        response.error || (response.result && response.result.error);

      if (!error && typeof callback === "function") {
        return callback(response.result);
      }

      if (error && typeof errorCallback === "function") {
        return errorCallback(error);
      }

      var errorEvent = new CustomEvent("unhandledError", {
        detail: {
          connectionString: connectionString,
          method: method,
          params: params,
          code: error.code,
          message: error.message || error,
          data: error.data,
        },
      });
      document.dispatchEvent(errorEvent);
    })
    .catch(e => {
      if (connectFailedCallback) {
        return connectFailedCallback(e);
      }

      var errorEvent = new CustomEvent("unhandledError", {
        detail: {
          connectionString: connectionString,
          method: method,
          params: params,
          code: e.response && e.response.status,
          message: __("Connection to API server failed"),
        },
      });
      document.dispatchEvent(errorEvent);
    });
};

export default jsonrpc;
<|endoftext|>


<commit_before>/**
 * A simple canvas renderer.
 * Renders circles and convex-polygons
 */
Physics.renderer('canvas', function( proto ){

    var Pi2 = Math.PI * 2
        // helper to create new dom elements
        ,newEl = function( node, content ){
            var el = document.createElement(node || 'div');
            if (content){
                el.innerHTML = content;
            }
            return el;
        }
        ;

    var defaults = {

        // draw aabbs of bodies for debugging
        debug: false,
        // the element to place meta data into
        metaEl: null,
        // default styles of drawn objects
        styles: {

            'point' : 'rgba(80, 50, 100, 0.7)',

            'circle' : {
                strokeStyle: 'rgba(70, 50, 100, 0.7)',
                lineWidth: 1,
                fillStyle: 'rgba(44, 105, 44, 0.7)',
                angleIndicator: 'rgba(69, 51, 78, 0.7)'
            },

            'convex-polygon' : {
                strokeStyle: 'rgba(80, 50, 100, 0.7)',
                lineWidth: 1,
                fillStyle: 'rgba(114, 105, 124, 0.7)',
                angleIndicator: 'rgba(69, 51, 78, 0.7)'
            }
        },
        offset: {x: 0, y: 0}
    };

    // deep copy callback to extend deeper into options
    var deep = function( a, b ){

        if ( Physics.util.isPlainObject( b ) ){

            return Physics.util.extend({}, a, b, deep );
        }

        return b !== undefined ? b : a;
    };

    return {

        /**
         * Initialization
         * @param  {Object} options Config options passed by initializer
         * @return {void}
         */
        init: function( options ){

            // call proto init
            proto.init.call(this, options);

            // further options
            this.options = Physics.util.extend({}, defaults, this.options, deep);
            this.options.offset = Physics.vector( this.options.offset );

            // hidden canvas
            this.hiddenCanvas = document.createElement('canvas');
            this.hiddenCanvas.width = this.hiddenCanvas.height = 100;

            if (!this.hiddenCanvas.getContext){
                throw "Canvas not supported";
            }

            this.hiddenCtx = this.hiddenCanvas.getContext('2d');

            // actual viewport
            var viewport = this.el;
            if (viewport.nodeName.toUpperCase() !== "CANVAS"){

                viewport = document.createElement('canvas');
                this.el.appendChild( viewport );
                this.el = viewport;
            }

            viewport.width = this.options.width;
            viewport.height = this.options.height;

            this.ctx = viewport.getContext("2d");

            this.els = {};

            if (this.options.meta){
                var stats = this.options.metaEl || newEl();
                stats.className = 'pjs-meta';
                this.els.fps = newEl('span');
                this.els.ipf = newEl('span');
                stats.appendChild(newEl('span', 'fps: '));
                stats.appendChild(this.els.fps);
                stats.appendChild(newEl('br'));
                stats.appendChild(newEl('span', 'ipf: '));
                stats.appendChild(this.els.ipf);

                viewport.parentNode.insertBefore(stats, viewport);
            }
        },

        /**
         * Set the styles of specified context
         * @param {Object|String} styles Styles configuration for body drawing
         * @param {Canvas2DContext} ctx    (optional) Defaults to visible canvas context
         */
        setStyle: function( styles, ctx ){

            ctx = ctx || this.ctx;

            if ( Physics.util.isObject(styles) ){

                ctx.strokeStyle = styles.strokeStyle;
                ctx.fillStyle = styles.fillStyle;
                ctx.lineWidth = styles.lineWidth;

            } else {

                ctx.fillStyle = ctx.strokeStyle = styles;
                ctx.lineWidth = 1;
            }
        },

        /**
         * Draw a circle to specified canvas context
         * @param  {Number} x      The x coord
         * @param  {Number} y      The y coord
         * @param  {Number} r      The circle radius
         * @param  {Object|String} styles The styles configuration
         * @param  {Canvas2DContext} ctx    (optional) The canvas context
         * @return {void}
         */
        drawCircle: function(x, y, r, styles, ctx){

            ctx = ctx || this.ctx;

            ctx.beginPath();
            this.setStyle( styles, ctx );
            ctx.arc(x, y, r, 0, Pi2, false);
            ctx.closePath();
            ctx.stroke();
            ctx.fill();
        },

        /**
         * Draw a polygon to specified canvas context
         * @param  {Array} verts  Array of vectorish vertices
         * @param  {Object|String} styles The styles configuration
         * @param  {Canvas2DContext} ctx    (optional) The canvas context
         * @return {void}
         */
        drawPolygon: function(verts, styles, ctx){

            var vert = verts[0]
                ,x = vert.x === undefined ? vert.get(0) : vert.x
                ,y = vert.y === undefined ? vert.get(1) : vert.y
                ,l = verts.length
                ;

            ctx = ctx || this.ctx;
            ctx.beginPath();
            this.setStyle( styles, ctx );

            ctx.moveTo(x, y);

            for ( var i = 1; i < l; ++i ){

                vert = verts[ i ];
                x = vert.x === undefined ? vert.get(0) : vert.x;
                y = vert.y === undefined ? vert.get(1) : vert.y;
                ctx.lineTo(x, y);
            }

            if (l > 2){
                ctx.closePath();
            }

            ctx.stroke();
            ctx.fill();
        },

        /**
         * Draw a line onto specified canvas context
         * @param  {Vectorish} from   Starting point
         * @param  {Vectorish} to     Ending point
         * @param  {Object|String} styles The styles configuration
         * @param  {Canvas2DContext} ctx    (optional) The canvas context
         * @return {void}
         */
        drawLine: function(from, to, styles, ctx){

            var x = from.x === undefined ? from.get(0) : from.x
                ,y = from.y === undefined ? from.get(1) : from.y
                ;

            ctx = ctx || this.ctx;

            ctx.beginPath();
            this.setStyle( styles, ctx );

            ctx.moveTo(x, y);

            x = to.x === undefined ? to.get(0) : to.x;
            y = to.y === undefined ? to.get(1) : to.y;

            ctx.lineTo(x, y);

            ctx.stroke();
            ctx.fill();
        },

        /**
         * Create a view for specified geometry.
         * @param  {Geometry} geometry The geometry
         * @param  {Object|String} styles The styles configuration
         * @return {Image}          An image cache of the geometry
         */
        createView: function( geometry, styles ){

            var view = new Image()
                ,aabb = geometry.aabb()
                ,hw = aabb.halfWidth + Math.abs(aabb.pos.x)
                ,hh = aabb.halfHeight + Math.abs(aabb.pos.y)
                ,x = hw + 1
                ,y = hh + 1
                ,hiddenCtx = this.hiddenCtx
                ,hiddenCanvas = this.hiddenCanvas
                ,name = geometry.name
                ;

            styles = styles || this.options.styles[ name ];

            x += styles.lineWidth | 0;
            y += styles.lineWidth | 0;

            // clear
            hiddenCanvas.width = 2 * hw + 2 + (2 * styles.lineWidth|0);
            hiddenCanvas.height = 2 * hh + 2 + (2 * styles.lineWidth|0);

            hiddenCtx.save();
            hiddenCtx.translate(x, y);

            if (name === 'circle'){

                this.drawCircle(0, 0, geometry.radius, styles, hiddenCtx);

            } else if (name === 'convex-polygon'){

                this.drawPolygon(geometry.vertices, styles, hiddenCtx);
            }

            if (styles.angleIndicator){

                hiddenCtx.beginPath();
                this.setStyle( styles.angleIndicator, hiddenCtx );
                hiddenCtx.moveTo(0, 0);
                hiddenCtx.lineTo(hw, 0);
                hiddenCtx.closePath();
                hiddenCtx.stroke();
            }

            hiddenCtx.restore();

            view.src = hiddenCanvas.toDataURL("image/png");
            view.width = hiddenCanvas.width;
            view.height = hiddenCanvas.height;
            return view;
        },

        /**
         * Draw the meta data
         * @param  {Object} meta The meta data
         * @return {void}
         */
        drawMeta: function( meta ){

            this.els.fps.innerHTML = meta.fps.toFixed(2);
            this.els.ipf.innerHTML = meta.ipf;
        },

        /**
         * Callback to be run before rendering
         * @private
         * @return {void}
         */
        beforeRender: function(){

            // clear canvas
            this.el.width = this.el.width;
        },

        /**
         * Draw a body to canvas
         * @param  {Body} body The body to draw
         * @param  {Image} view The view for that body
         * @return {void}
         */
        drawBody: function( body, view ){

            var ctx = this.ctx
                ,pos = body.state.pos
                ,offset = this.options.offset
                ,aabb = body.aabb()
                ;

            ctx.save();
            ctx.translate(pos.get(0) + offset.get(0), pos.get(1) + offset.get(1));
            ctx.rotate(body.state.angular.pos);
            ctx.drawImage(view, -view.width/2, -view.height/2);
            ctx.restore();

            if ( this.options.debug ){
                // draw bounding boxes
                ctx.save();
                ctx.translate(offset.get(0), offset.get(1));
                this.drawPolygon([
                        { x: aabb.pos.x - aabb.x, y: aabb.pos.y - aabb.y },
                        { x: aabb.pos.x + aabb.x, y: aabb.pos.y - aabb.y },
                        { x: aabb.pos.x + aabb.x, y: aabb.pos.y + aabb.y },
                        { x: aabb.pos.x - aabb.x, y: aabb.pos.y + aabb.y }
                    ], 'rgba(100, 255, 100, 0.3)');
                ctx.restore();
            }
        }
    };
});
<commit_msg>minor optimization of canvas clearing<commit_after>/**
 * A simple canvas renderer.
 * Renders circles and convex-polygons
 */
Physics.renderer('canvas', function( proto ){

    var Pi2 = Math.PI * 2
        // helper to create new dom elements
        ,newEl = function( node, content ){
            var el = document.createElement(node || 'div');
            if (content){
                el.innerHTML = content;
            }
            return el;
        }
        ;

    var defaults = {

        // draw aabbs of bodies for debugging
        debug: false,
        // the element to place meta data into
        metaEl: null,
        // default styles of drawn objects
        styles: {

            'point' : 'rgba(80, 50, 100, 0.7)',

            'circle' : {
                strokeStyle: 'rgba(70, 50, 100, 0.7)',
                lineWidth: 1,
                fillStyle: 'rgba(44, 105, 44, 0.7)',
                angleIndicator: 'rgba(69, 51, 78, 0.7)'
            },

            'convex-polygon' : {
                strokeStyle: 'rgba(80, 50, 100, 0.7)',
                lineWidth: 1,
                fillStyle: 'rgba(114, 105, 124, 0.7)',
                angleIndicator: 'rgba(69, 51, 78, 0.7)'
            }
        },
        offset: {x: 0, y: 0}
    };

    // deep copy callback to extend deeper into options
    var deep = function( a, b ){

        if ( Physics.util.isPlainObject( b ) ){

            return Physics.util.extend({}, a, b, deep );
        }

        return b !== undefined ? b : a;
    };

    return {

        /**
         * Initialization
         * @param  {Object} options Config options passed by initializer
         * @return {void}
         */
        init: function( options ){

            // call proto init
            proto.init.call(this, options);

            // further options
            this.options = Physics.util.extend({}, defaults, this.options, deep);
            this.options.offset = Physics.vector( this.options.offset );

            // hidden canvas
            this.hiddenCanvas = document.createElement('canvas');
            this.hiddenCanvas.width = this.hiddenCanvas.height = 100;

            if (!this.hiddenCanvas.getContext){
                throw "Canvas not supported";
            }

            this.hiddenCtx = this.hiddenCanvas.getContext('2d');

            // actual viewport
            var viewport = this.el;
            if (viewport.nodeName.toUpperCase() !== "CANVAS"){

                viewport = document.createElement('canvas');
                this.el.appendChild( viewport );
                this.el = viewport;
            }

            viewport.width = this.options.width;
            viewport.height = this.options.height;

            this.ctx = viewport.getContext("2d");

            this.els = {};

            if (this.options.meta){
                var stats = this.options.metaEl || newEl();
                stats.className = 'pjs-meta';
                this.els.fps = newEl('span');
                this.els.ipf = newEl('span');
                stats.appendChild(newEl('span', 'fps: '));
                stats.appendChild(this.els.fps);
                stats.appendChild(newEl('br'));
                stats.appendChild(newEl('span', 'ipf: '));
                stats.appendChild(this.els.ipf);

                viewport.parentNode.insertBefore(stats, viewport);
            }
        },

        /**
         * Set the styles of specified context
         * @param {Object|String} styles Styles configuration for body drawing
         * @param {Canvas2DContext} ctx    (optional) Defaults to visible canvas context
         */
        setStyle: function( styles, ctx ){

            ctx = ctx || this.ctx;

            if ( Physics.util.isObject(styles) ){

                ctx.strokeStyle = styles.strokeStyle;
                ctx.fillStyle = styles.fillStyle;
                ctx.lineWidth = styles.lineWidth;

            } else {

                ctx.fillStyle = ctx.strokeStyle = styles;
                ctx.lineWidth = 1;
            }
        },

        /**
         * Draw a circle to specified canvas context
         * @param  {Number} x      The x coord
         * @param  {Number} y      The y coord
         * @param  {Number} r      The circle radius
         * @param  {Object|String} styles The styles configuration
         * @param  {Canvas2DContext} ctx    (optional) The canvas context
         * @return {void}
         */
        drawCircle: function(x, y, r, styles, ctx){

            ctx = ctx || this.ctx;

            ctx.beginPath();
            this.setStyle( styles, ctx );
            ctx.arc(x, y, r, 0, Pi2, false);
            ctx.closePath();
            ctx.stroke();
            ctx.fill();
        },

        /**
         * Draw a polygon to specified canvas context
         * @param  {Array} verts  Array of vectorish vertices
         * @param  {Object|String} styles The styles configuration
         * @param  {Canvas2DContext} ctx    (optional) The canvas context
         * @return {void}
         */
        drawPolygon: function(verts, styles, ctx){

            var vert = verts[0]
                ,x = vert.x === undefined ? vert.get(0) : vert.x
                ,y = vert.y === undefined ? vert.get(1) : vert.y
                ,l = verts.length
                ;

            ctx = ctx || this.ctx;
            ctx.beginPath();
            this.setStyle( styles, ctx );

            ctx.moveTo(x, y);

            for ( var i = 1; i < l; ++i ){

                vert = verts[ i ];
                x = vert.x === undefined ? vert.get(0) : vert.x;
                y = vert.y === undefined ? vert.get(1) : vert.y;
                ctx.lineTo(x, y);
            }

            if (l > 2){
                ctx.closePath();
            }

            ctx.stroke();
            ctx.fill();
        },

        /**
         * Draw a line onto specified canvas context
         * @param  {Vectorish} from   Starting point
         * @param  {Vectorish} to     Ending point
         * @param  {Object|String} styles The styles configuration
         * @param  {Canvas2DContext} ctx    (optional) The canvas context
         * @return {void}
         */
        drawLine: function(from, to, styles, ctx){

            var x = from.x === undefined ? from.get(0) : from.x
                ,y = from.y === undefined ? from.get(1) : from.y
                ;

            ctx = ctx || this.ctx;

            ctx.beginPath();
            this.setStyle( styles, ctx );

            ctx.moveTo(x, y);

            x = to.x === undefined ? to.get(0) : to.x;
            y = to.y === undefined ? to.get(1) : to.y;

            ctx.lineTo(x, y);

            ctx.stroke();
            ctx.fill();
        },

        /**
         * Create a view for specified geometry.
         * @param  {Geometry} geometry The geometry
         * @param  {Object|String} styles The styles configuration
         * @return {Image}          An image cache of the geometry
         */
        createView: function( geometry, styles ){

            var view = new Image()
                ,aabb = geometry.aabb()
                ,hw = aabb.halfWidth + Math.abs(aabb.pos.x)
                ,hh = aabb.halfHeight + Math.abs(aabb.pos.y)
                ,x = hw + 1
                ,y = hh + 1
                ,hiddenCtx = this.hiddenCtx
                ,hiddenCanvas = this.hiddenCanvas
                ,name = geometry.name
                ;

            styles = styles || this.options.styles[ name ];

            x += styles.lineWidth | 0;
            y += styles.lineWidth | 0;

            // clear
            hiddenCanvas.width = 2 * hw + 2 + (2 * styles.lineWidth|0);
            hiddenCanvas.height = 2 * hh + 2 + (2 * styles.lineWidth|0);

            hiddenCtx.save();
            hiddenCtx.translate(x, y);

            if (name === 'circle'){

                this.drawCircle(0, 0, geometry.radius, styles, hiddenCtx);

            } else if (name === 'convex-polygon'){

                this.drawPolygon(geometry.vertices, styles, hiddenCtx);
            }

            if (styles.angleIndicator){

                hiddenCtx.beginPath();
                this.setStyle( styles.angleIndicator, hiddenCtx );
                hiddenCtx.moveTo(0, 0);
                hiddenCtx.lineTo(hw, 0);
                hiddenCtx.closePath();
                hiddenCtx.stroke();
            }

            hiddenCtx.restore();

            view.src = hiddenCanvas.toDataURL("image/png");
            view.width = hiddenCanvas.width;
            view.height = hiddenCanvas.height;
            return view;
        },

        /**
         * Draw the meta data
         * @param  {Object} meta The meta data
         * @return {void}
         */
        drawMeta: function( meta ){

            this.els.fps.innerHTML = meta.fps.toFixed(2);
            this.els.ipf.innerHTML = meta.ipf;
        },

        /**
         * Callback to be run before rendering
         * @private
         * @return {void}
         */
        beforeRender: function(){

            // clear canvas
            this.ctx.clearRect(0, 0, this.el.width, this.el.height);
        },

        /**
         * Draw a body to canvas
         * @param  {Body} body The body to draw
         * @param  {Image} view The view for that body
         * @return {void}
         */
        drawBody: function( body, view ){

            var ctx = this.ctx
                ,pos = body.state.pos
                ,offset = this.options.offset
                ,aabb = body.aabb()
                ;

            ctx.save();
            ctx.translate(pos.get(0) + offset.get(0), pos.get(1) + offset.get(1));
            ctx.rotate(body.state.angular.pos);
            ctx.drawImage(view, -view.width/2, -view.height/2);
            ctx.restore();

            if ( this.options.debug ){
                // draw bounding boxes
                ctx.save();
                ctx.translate(offset.get(0), offset.get(1));
                this.drawPolygon([
                        { x: aabb.pos.x - aabb.x, y: aabb.pos.y - aabb.y },
                        { x: aabb.pos.x + aabb.x, y: aabb.pos.y - aabb.y },
                        { x: aabb.pos.x + aabb.x, y: aabb.pos.y + aabb.y },
                        { x: aabb.pos.x - aabb.x, y: aabb.pos.y + aabb.y }
                    ], 'rgba(100, 255, 100, 0.3)');
                ctx.restore();
            }
        }
    };
});
<|endoftext|>


<commit_before>var path = require('path');
var fs = require('fs');
var template = require('string-template');
var readJSON = require('read-json');
var url = require('url');

var NPM_URI = 'https://registry.npmjs.org/{name}/-/{name}-{version}.tgz';

module.exports = setResolved;

function defaultCreateUri(name, version) {
    return template(NPM_URI, {
        name: name,
        version: version
    });
}

/*  from field is either:
     - {name}@{semverRange}
     - {name}@{gitUri}
     - {privateRegistryUri}

*/
function setResolved(opts, callback) {
    if (typeof opts === 'string') {
        opts = { dirname: opts };
    }

    var shrinkwrapFile = path.join(opts.dirname, 'npm-shrinkwrap.json');
    var createUri = opts.createUri || defaultCreateUri;
    var registries = opts.registries || ['registry.npmjs.org'];
    var rewriteResolved = opts.rewriteResolved || null;

    readJSON(shrinkwrapFile, onjson);

    function onjson(err, json) {
        if (err) {
            return callback(err);
        }

        json = fixResolved(json);

        // if top level shrinkwrap has a `from` or `resolved`
        // field then delete them
        if (json.from) {
            json.from = undefined;
        }
        if (json.resolved) {
            json.resolved = undefined;
        }

        fs.writeFile(shrinkwrapFile,
            JSON.stringify(json, null, 2), callback);
    }

    function fixResolved(json, name) {
        if (json.from && !json.resolved) {
            computeResolved(json, name);
        }

        if (rewriteResolved) {
            json.resolved = rewriteResolved(json.resolved);
        }

        if (json.dependencies) {
            Object.keys(json.dependencies).forEach(function (dep) {
                fixResolved(json.dependencies[dep], dep);
            });
        }

        return json;
    }

    /*  look for `from` fields and set a `resolved` field next
          to it if the `resolved` does not exist.

        This normalizes `npm shrinkwrap` so a resolved field
          always get's set.

    */
    function computeResolved(json, name) {
        var value = json.from;
        name = name || json.name;

        var uri = url.parse(value);

        // handle the case `from` is a privateRegistryURL
        if ((uri.protocol === 'http:' || uri.protocol === 'https:') &&
            registries.indexOf(uri.host) !== -1
        ) {
            json.resolved = value;
            return;
        }

        // from is {name}@{semver | uri}
        var parts = value.split('@');
        var rest = parts.slice(1).join('@');

        var secondUri = url.parse(rest);

        // from is a {name}@{semver}
        if (!secondUri.protocol) {
            // call createUri to generate a tarball uri
            // for json module name & version
            json.resolved = createUri(name, json.version);
            return;
        } else {
            // from is a git link.
            // do not try to set resolved
            return;
        }
    }
}
<commit_msg>gaurd against null resolved<commit_after>var path = require('path');
var fs = require('fs');
var template = require('string-template');
var readJSON = require('read-json');
var url = require('url');

var NPM_URI = 'https://registry.npmjs.org/{name}/-/{name}-{version}.tgz';

module.exports = setResolved;

function defaultCreateUri(name, version) {
    return template(NPM_URI, {
        name: name,
        version: version
    });
}

/*  from field is either:
     - {name}@{semverRange}
     - {name}@{gitUri}
     - {privateRegistryUri}

*/
function setResolved(opts, callback) {
    if (typeof opts === 'string') {
        opts = { dirname: opts };
    }

    var shrinkwrapFile = path.join(opts.dirname, 'npm-shrinkwrap.json');
    var createUri = opts.createUri || defaultCreateUri;
    var registries = opts.registries || ['registry.npmjs.org'];
    var rewriteResolved = opts.rewriteResolved || null;

    readJSON(shrinkwrapFile, onjson);

    function onjson(err, json) {
        if (err) {
            return callback(err);
        }

        json = fixResolved(json);

        // if top level shrinkwrap has a `from` or `resolved`
        // field then delete them
        if (json.from) {
            json.from = undefined;
        }
        if (json.resolved) {
            json.resolved = undefined;
        }

        fs.writeFile(shrinkwrapFile,
            JSON.stringify(json, null, 2), callback);
    }

    function fixResolved(json, name) {
        if (json.from && !json.resolved) {
            computeResolved(json, name);
        }

        if (rewriteResolved && json.resolved) {
            json.resolved = rewriteResolved(json.resolved);
        }

        if (json.dependencies) {
            Object.keys(json.dependencies).forEach(function (dep) {
                fixResolved(json.dependencies[dep], dep);
            });
        }

        return json;
    }

    /*  look for `from` fields and set a `resolved` field next
          to it if the `resolved` does not exist.

        This normalizes `npm shrinkwrap` so a resolved field
          always get's set.

    */
    function computeResolved(json, name) {
        var value = json.from;
        name = name || json.name;

        var uri = url.parse(value);

        // handle the case `from` is a privateRegistryURL
        if ((uri.protocol === 'http:' || uri.protocol === 'https:') &&
            registries.indexOf(uri.host) !== -1
        ) {
            json.resolved = value;
            return;
        }

        // from is {name}@{semver | uri}
        var parts = value.split('@');
        var rest = parts.slice(1).join('@');

        var secondUri = url.parse(rest);

        // from is a {name}@{semver}
        if (!secondUri.protocol) {
            // call createUri to generate a tarball uri
            // for json module name & version
            json.resolved = createUri(name, json.version);
            return;
        } else {
            // from is a git link.
            // do not try to set resolved
            return;
        }
    }
}
<|endoftext|>


<commit_before>const dbCmds = require('../db/commands.js')
const moment = require('moment-timezone')
const defaultConfigs = require('../../util/checkConfig.js').defaultConfigs
const log = require('../../util/logger.js')
const Article = require('../../models/Article.js')
const ArticleIDResolver = require('../../structs/ArticleIDResolver.js')

function formatCallbackArticle (article, source, rssName, callbackFunc) {
  // For ArticleMessage to access once ScheduleManager receives this article
  article._delivery = {
    guildId: source.guildId, // Originally set in source through FeedSchedule.js
    dateSettings: source.dateSettings, // Not the actual date, but settings concerning date format/language/timezone. Originally set in source through FeedSchedule.js
    rssName,
    channelId: source.channel,
    source
  }

  callbackFunc({ status: 'article', article })
}

module.exports = async (data, callbackArticle) => {
  const { rssList, articleList, debugFeeds, link, shardId, config, feedData, scheduleName, runNum, useIdType } = data // feedData is only defined when config.database.uri is set to a databaseless folder path
  if (!scheduleName) throw new Error('Missing schedule name for shared logic')
  const totalArticles = articleList.length
  const dbIds = new Set()
  const dbTitles = new Set()
  const dbCustomComparisons = {} // Object with comparison names as key, and array as values whose function is similar to how dbIds and dbTitles work
  const dbCustomComparisonsValid = {} // Object with comparison names as key, and only boolean true values as values
  const dbCustomComparisonsToDelete = []
  const customComparisonsToUpdate = []
  const toInsert = []
  const toUpdate = {} // Article's resolved IDs as key and the article as value
  const collectionID = Article.getCollectionID(link, shardId, scheduleName)
  const Feed = Article.modelByID(collectionID)
  const feedCollectionId = feedData ? collectionID : undefined
  const feedCollection = feedData ? (feedData[feedCollectionId] || []) : undefined

  const docs = await dbCmds.findAll(feedCollection || Feed)
  for (const doc of docs) {
    // Push the main data for built in comparisons
    dbIds.add(doc.id)
    dbTitles.add(doc.title)

    // Now deal with custom comparisons
    const docCustomComparisons = doc.customComparisons
    if (docCustomComparisons !== undefined && Object.keys(docCustomComparisons).length > 0) {
      for (const n in docCustomComparisons) { // n = customComparison's name (such as description, author, etc.)
        if (!dbCustomComparisons[n]) {
          dbCustomComparisons[n] = [docCustomComparisons[n]]
        } else {
          dbCustomComparisons[n].push(docCustomComparisons[n])
        }
      }
    }
  }

  const checkCustomComparisons = Object.keys(dbCustomComparisons).length > 0
  for (const article of articleList) {
    article._id = ArticleIDResolver.getIDTypeValue(article, useIdType)
    if (checkCustomComparisons) {
    // Iterate over the values stored in the db, and see if the custom comparison names in the db exist in any of the articles. If they do, then it is marked valid
      for (const compName in dbCustomComparisons) {
        if (article[compName] !== undefined && (typeof article[compName] !== 'object' || article[compName] === null)) {
          dbCustomComparisonsValid[compName] = true
        }
      }
    }
    if (!dbIds.has(article._id)) {
      toInsert.push(article)
    }
  }

  // If any invalid custom comparisons are found, delete them
  if (checkCustomComparisons) {
    for (const q in dbCustomComparisons) {
      if (!dbCustomComparisonsValid[q]) {
        dbCustomComparisonsToDelete.push(q)
        delete dbCustomComparisons[q]
      }
    }
  }
  await dbCmds.bulkInsert(feedCollection || Feed, toInsert)
  if (dbIds.size === 0) {
    return { status: 'success', link, feedCollection, feedCollectionId }
  }
  for (const rssName in rssList) {
    const source = rssList[rssName]
    const toDebug = debugFeeds && debugFeeds.includes(rssName)
    const customComparisons = source.customComparisons // Array of names
    const sentTitles = new Set()

    if (Array.isArray(customComparisons)) {
      for (let n = customComparisons.length - 1; n >= 0; --n) {
        const name = customComparisons[n]
        if (name === 'title' || name === 'guid' || name === 'pubdate') { // Forbidden custom comparisons since these are already used by the bot
          customComparisons.splice(n, 1)
          continue
        }
        if (!dbCustomComparisons[name] && !customComparisonsToUpdate.includes(name)) {
          customComparisonsToUpdate.push(name) // Since this custom comparison wasn't found in the db, it might be uninitialized or not found in any articles (as checked previously)
        }
      }
    }

    if (toDebug) {
      log.debug.info(`${rssName}: Processing collection. Total article list length: ${totalArticles}`)
    }

    const maxAge = config.feeds.cycleMaxAge
    const cutoffDay = moment().subtract(maxAge, 'days')

    const globalDateCheck = config.feeds.checkDates != null ? config.feeds.checkDates : defaultConfigs.feeds.checkDates.default
    const localDateCheck = source.checkDates
    const checkDate = typeof localDateCheck !== 'boolean' ? globalDateCheck : localDateCheck

    const globalTitleCheck = config.feeds.checkTitles != null ? config.feeds.checkTitles : defaultConfigs.feeds.checkTitles.default
    const localTitleCheck = source.checkTitles
    const checkTitle = typeof globalTitleCheck !== 'boolean' ? globalTitleCheck : localTitleCheck

    if (toDebug) {
      log.debug.info('Database IDs:', JSON.stringify(Array.from(dbIds)))
      log.debug.info('Database Titles:', JSON.stringify(Array.from(dbTitles)))
    }
    for (let a = totalArticles - 1; a >= 0; --a) { // Loop from oldest to newest so the queue that sends articleMessages work properly, sending the older ones first
      const article = articleList[a]
      const notInitialized = dbIds.size === 0 && totalArticles !== 1
      const matchedID = dbIds.has(article._id)
      const matchedTitle = checkTitle && (dbTitles.has(article.title) || sentTitles.has(article.title))
      const matchedDate = checkDate && ((!article.pubdate || article.pubdate.toString() === 'Invalid Date') || (article.pubdate && article.pubdate.toString() !== 'Invalid Date' && article.pubdate < cutoffDay))
      let seen = false
      if (notInitialized || matchedID || matchedTitle || matchedDate) {
        if (toDebug) log.debug.info(`${rssName}: Not sending article (ID: ${article._id}, TITLE: ${article.title}) Matched ${notInitialized ? 'init case' : matchedID ? 'ID' : matchedTitle ? 'title' : matchedDate ? 'date' : 'UNKNOWN CASE'}.`)
        seen = true
      } else if (checkTitle && article.title) {
        sentTitles.add(article.title)
      }

      if (runNum === 0 && config.feeds.sendOldOnFirstCycle === false) {
        if (debugFeeds && debugFeeds.includes(rssName)) {
          log.debug.warning(`${rssName}: Not sending article (ID: ${article._id}, TITLE: ${article.title}), config.feeds.sendOldOnFirstCycle is false`)
        }
        continue
      }

      // Check for extra user-specified comparisons
      if (!seen) {
        formatCallbackArticle(article, source, rssName, callbackArticle)
        continue
      }
      if (!Array.isArray(customComparisons)) {
        continue
      }
      for (const comparisonName of customComparisons) {
        const dbCustomComparisonValues = dbCustomComparisons[comparisonName] // Might be an array of descriptions, authors, etc.
        const articleCustomComparisonValue = article[comparisonName]
        if (!dbCustomComparisonValues || dbCustomComparisonValues.includes(articleCustomComparisonValue) || !articleCustomComparisonValue) {
          if (debugFeeds && debugFeeds.includes(rssName)) {
            log.debug.info(`${rssName}: Not sending article (ID: ${article._id}, TITLE: ${article.title}) due to custom comparison check for ${comparisonName}${!articleCustomComparisonValue ? ' (No article value for custom comparison field)' : ''}`)
          }
          if (debugFeeds && debugFeeds.includes(rssName)) {
            log.debug.info(`${rssName}: (ID: ${article._id}, TITLE: ${article.title}) ${comparisonName} dbCustomComparisonValues: ${dbCustomComparisonValues ? JSON.stringify(dbCustomComparisonValues) : undefined} `)
          }
          continue // The comparison must either be uninitialized or invalid (no such comparison exists in any articles from the request), handled by a previous function. OR it exists in the db
        }

        // Prepare it for update in the database
        if (!toUpdate[article._id]) {
          if (!article.customComparisons) article.customComparisons = {}
          article.customComparisons[comparisonName] = articleCustomComparisonValue
          toUpdate[article._id] = article
        }
        if (debugFeeds && debugFeeds.includes(rssName)) {
          log.debug.info(`${rssName}: Sending article (ID: ${article._id}, TITLE: ${article.title}) due to custom comparison check for ${comparisonName}`)
        }
        formatCallbackArticle(article, source, rssName, callbackArticle)
      }
    }
  }

  const mustUpdate = customComparisonsToUpdate.length
  for (let l = 0; l < mustUpdate; ++l) {
    const customComparisonName = customComparisonsToUpdate[l]
    for (const article of articleList) {
      const articleCustomComparisonValue = article[customComparisonName]
      if (articleCustomComparisonValue === undefined || (typeof articleCustomComparisonValue === 'object' && articleCustomComparisonValue !== null)) {
        continue // typeof null returns 'object' even though it's not supposed to
      }
      if (!toUpdate[article._id]) {
        if (!article.customComparisons) {
          article.customComparisons = {}
        }
        article.customComparisons[customComparisonName] = articleCustomComparisonValue
        toUpdate[article._id] = article
      } else {
        toUpdate[article._id].customComparisons[customComparisonName] = articleCustomComparisonValue
      }
    }
  }

  // Update anything if necessary
  const updates = []
  for (const id in toUpdate) {
    const article = toUpdate[id]
    updates.push(dbCmds.update(feedCollection || Feed, article))
  }
  await Promise.all(updates)
  return { status: 'success', link, feedCollection, feedCollectionId }
}
<commit_msg>Separate module into more functions for shared logic<commit_after>const dbCmds = require('../db/commands.js')
const moment = require('moment-timezone')
const defaultConfigs = require('../../util/checkConfig.js').defaultConfigs
const log = require('../../util/logger.js')
const Article = require('../../models/Article.js')
const ArticleIDResolver = require('../../structs/ArticleIDResolver.js')

/**
 * @param {object} article
 * @param {object} source
 * @param {string} rssName
 * @param {function} callbackFunc
 */
function formatCallbackArticle (article, source, rssName, callbackFunc) {
  // For ArticleMessage to access once ScheduleManager receives this article
  article._delivery = {
    guildId: source.guildId, // Originally set in source through FeedSchedule.js
    dateSettings: source.dateSettings, // Not the actual date, but settings concerning date format/language/timezone. Originally set in source through FeedSchedule.js
    rssName,
    channelId: source.channel,
    source
  }

  callbackFunc({ status: 'article', article })
}

/**
 * @typedef {object} DatabaseData
 * @property {Set<string>} dbIds - Set of stored database IDs
 * @property {Set<string>} dbTitles - Set of stored database titles
 * @property {object<string, string[]>} dbCustomComparisons - Comparison names as keys, and their relevant article values in arrays
 */

/**
 * @param {import('mongoose').Model|object[]} collection
 * @returns {DatabaseData}
 */
async function getDataFromDocuments (collection) {
  const dbIds = new Set()
  const dbTitles = new Set()
  /** @type {object<string, string[]} */
  const dbCustomComparisons = {}
  const docs = await dbCmds.findAll(collection)
  for (const doc of docs) {
    // Push the main data for built in comparisons
    dbIds.add(doc.id)
    dbTitles.add(doc.title)

    // Now deal with custom comparisons
    const docCustomComparisons = doc.customComparisons
    if (docCustomComparisons !== undefined && Object.keys(docCustomComparisons).length > 0) {
      for (const articleProperty in docCustomComparisons) { // n = customComparison's name (such as description, author, etc.)
        const value = docCustomComparisons[articleProperty]
        if (!dbCustomComparisons[articleProperty]) {
          dbCustomComparisons[articleProperty] = [value]
        } else {
          dbCustomComparisons[articleProperty].push(value)
        }
      }
    }
  }

  return { dbIds, dbTitles, dbCustomComparisons }
}

/**
 * @param {object[]} collection
 * @param {Set<string>} dbIds
 * @param {string} useIdType
 * @param {object[]} articleList
 * @param {object<string, string[]>} dbCustomComparisons
 * @returns {Set<string>}
 */
async function articleListTasks (collection, dbIds, useIdType, articleList, dbCustomComparisons) {
  const toInsert = []
  const dbCustomComparisonsValid = new Set()
  const dbCustomComparisonsToDelete = new Set()

  const checkCustomComparisons = Object.keys(dbCustomComparisons).length > 0
  for (const article of articleList) {
    article._id = ArticleIDResolver.getIDTypeValue(article, useIdType)
    if (checkCustomComparisons) {
    // Iterate over the values stored in the db, and see if the custom comparison names in the db exist in any of the articles. If they do, then it is marked valid
      for (const compName in dbCustomComparisons) {
        if (article[compName] !== undefined && (typeof article[compName] !== 'object' || article[compName] === null)) {
          dbCustomComparisonsValid.add(compName)
        }
      }
    }
    if (!dbIds.has(article._id)) {
      toInsert.push(article)
    }
  }

  for (const articleProperty in dbCustomComparisons) {
    if (!dbCustomComparisonsValid.has(articleProperty)) {
      dbCustomComparisonsToDelete.add(articleProperty)
      delete dbCustomComparisons[articleProperty]
    }
  }

  await dbCmds.bulkInsert(collection, toInsert)

  return dbCustomComparisonsToDelete
}

/**
 * @param {object} config - The default config.json
 * @param {object} source - User's source config
 */
function determineArticleChecks (config, source) {
  const globalDateCheck = config.feeds.checkDates != null ? config.feeds.checkDates : defaultConfigs.feeds.checkDates.default
  const localDateCheck = source.checkDates
  const checkDate = typeof localDateCheck !== 'boolean' ? globalDateCheck : localDateCheck

  const globalTitleCheck = config.feeds.checkTitles != null ? config.feeds.checkTitles : defaultConfigs.feeds.checkTitles.default
  const localTitleCheck = source.checkTitles
  const checkTitle = typeof globalTitleCheck !== 'boolean' ? globalTitleCheck : localTitleCheck

  return { checkDate, checkTitle }
}

module.exports = async (data, callbackArticle) => {
  const { rssList, articleList, debugFeeds, link, shardId, config, feedData, scheduleName, runNum, useIdType } = data // feedData is only defined when config.database.uri is set to a databaseless folder path
  if (!scheduleName) throw new Error('Missing schedule name for shared logic')
  const totalArticles = articleList.length
  const customComparisonsToUpdate = []
  const toUpdate = {} // Article's resolved IDs as key and the article as value
  const collectionID = Article.getCollectionID(link, shardId, scheduleName)
  const Feed = Article.modelByID(collectionID)
  const feedCollectionId = feedData ? collectionID : undefined
  const feedCollection = feedData ? (feedData[feedCollectionId] || []) : undefined

  const { dbIds, dbTitles, dbCustomComparisons } = await getDataFromDocuments(feedCollection || Feed)
  const dbCustomComparisonsToDelete = await articleListTasks(dbIds, useIdType, articleList, dbCustomComparisons)

  if (dbIds.size === 0) {
    // Tthe database collection has not been initialized. If a feed has 100 articles, skip everything past this point so it doesn't send 100 articles.
    return { status: 'success', link, feedCollection, feedCollectionId }
  }

  for (const rssName in rssList) {
    const source = rssList[rssName]
    const toDebug = debugFeeds && debugFeeds.includes(rssName)
    const customComparisons = source.customComparisons // Array of names
    const sentTitles = new Set()

    if (Array.isArray(customComparisons)) {
      for (let n = customComparisons.length - 1; n >= 0; --n) {
        const name = customComparisons[n]
        if (name === 'title' || name === 'guid' || name === 'pubdate') { // Forbidden custom comparisons since these are already used by the bot
          customComparisons.splice(n, 1)
          continue
        }
        if (!dbCustomComparisons[name] && !dbCustomComparisonsToDelete.has(name) && !customComparisonsToUpdate.includes(name)) {
          customComparisonsToUpdate.push(name) // Since this custom comparison wasn't found in the db, it might be uninitialized or not found in any articles (as checked previously)
        }
      }
    }

    if (toDebug) {
      log.debug.info(`${rssName}: Processing collection. Total article list length: ${totalArticles}`)
    }

    const maxAge = config.feeds.cycleMaxAge
    const cutoffDay = moment().subtract(maxAge, 'days')

    const { checkDate, checkTitle } = determineArticleChecks

    if (toDebug) {
      log.debug.info('Database IDs:', JSON.stringify(Array.from(dbIds)))
      log.debug.info('Database Titles:', JSON.stringify(Array.from(dbTitles)))
    }
    for (let a = totalArticles - 1; a >= 0; --a) { // Loop from oldest to newest so the queue that sends articleMessages work properly, sending the older ones first
      const article = articleList[a]
      const matchedID = dbIds.has(article._id)
      const matchedTitle = checkTitle && (dbTitles.has(article.title) || sentTitles.has(article.title))
      const matchedDate = checkDate && ((!article.pubdate || article.pubdate.toString() === 'Invalid Date') || (article.pubdate && article.pubdate.toString() !== 'Invalid Date' && article.pubdate < cutoffDay))
      let seen = false
      if (matchedID || matchedTitle || matchedDate) {
        if (toDebug) log.debug.info(`${rssName}: Not sending article (ID: ${article._id}, TITLE: ${article.title}) Matched ${matchedID ? 'ID' : matchedTitle ? 'title' : matchedDate ? 'date' : 'UNKNOWN CASE'}.`)
        seen = true
      } else if (checkTitle && article.title) {
        sentTitles.add(article.title)
      }

      if (runNum === 0 && config.feeds.sendOldOnFirstCycle === false) {
        if (debugFeeds && debugFeeds.includes(rssName)) {
          log.debug.warning(`${rssName}: Not sending article (ID: ${article._id}, TITLE: ${article.title}), config.feeds.sendOldOnFirstCycle is false`)
        }
        continue
      }

      // Check for extra user-specified comparisons
      if (!seen) {
        formatCallbackArticle(article, source, rssName, callbackArticle)
        continue
      }
      if (!Array.isArray(customComparisons)) {
        continue
      }
      for (const comparisonName of customComparisons) {
        const dbCustomComparisonValues = dbCustomComparisons[comparisonName] // Might be an array of descriptions, authors, etc.
        const articleCustomComparisonValue = article[comparisonName]
        if (!dbCustomComparisonValues || dbCustomComparisonValues.includes(articleCustomComparisonValue) || !articleCustomComparisonValue) {
          if (debugFeeds && debugFeeds.includes(rssName)) {
            log.debug.info(`${rssName}: Not sending article (ID: ${article._id}, TITLE: ${article.title}) due to custom comparison check for ${comparisonName}${!articleCustomComparisonValue ? ' (No article value for custom comparison field)' : ''}`)
          }
          if (debugFeeds && debugFeeds.includes(rssName)) {
            log.debug.info(`${rssName}: (ID: ${article._id}, TITLE: ${article.title}) ${comparisonName} dbCustomComparisonValues: ${dbCustomComparisonValues ? JSON.stringify(dbCustomComparisonValues) : undefined} `)
          }
          continue // The comparison must either be uninitialized or invalid (no such comparison exists in any articles from the request), handled by a previous function. OR it exists in the db
        }

        // Prepare it for update in the database
        if (!toUpdate[article._id]) {
          if (!article.customComparisons) article.customComparisons = {}
          article.customComparisons[comparisonName] = articleCustomComparisonValue
          toUpdate[article._id] = article
        }
        if (debugFeeds && debugFeeds.includes(rssName)) {
          log.debug.info(`${rssName}: Sending article (ID: ${article._id}, TITLE: ${article.title}) due to custom comparison check for ${comparisonName}`)
        }
        formatCallbackArticle(article, source, rssName, callbackArticle)
      }
    }
  }

  const mustUpdate = customComparisonsToUpdate.length
  for (let l = 0; l < mustUpdate; ++l) {
    const customComparisonName = customComparisonsToUpdate[l]
    for (const article of articleList) {
      const articleCustomComparisonValue = article[customComparisonName]
      if (articleCustomComparisonValue === undefined || (typeof articleCustomComparisonValue === 'object' && articleCustomComparisonValue !== null)) {
        continue // typeof null returns 'object' even though it's not supposed to
      }
      if (!toUpdate[article._id]) {
        if (!article.customComparisons) {
          article.customComparisons = {}
        }
        article.customComparisons[customComparisonName] = articleCustomComparisonValue
        toUpdate[article._id] = article
      } else {
        toUpdate[article._id].customComparisons[customComparisonName] = articleCustomComparisonValue
      }
    }
  }

  // Update anything if necessary
  const updates = []
  for (const id in toUpdate) {
    const article = toUpdate[id]
    updates.push(dbCmds.update(feedCollection || Feed, article))
  }
  await Promise.all(updates)
  return { status: 'success', link, feedCollection, feedCollectionId }
}
<|endoftext|>


<commit_before>'use strict';

describe('myApp.craggy_island module', function() {
  var $compile;
  var $rootScope;
  var $q;
  var craggyIslandService;

  beforeEach(module('app.templates'));
  beforeEach(function() {
    craggyIslandService = jasmine.createSpyObj('craggyIslandService', ['sayHello', 'getDrink', 'getDirections']);
    craggyIslandService.getDrink.and.callThrough();
    craggyIslandService.getDirections.and.callThrough();
    module('myApp.hello', function($provide) {
      $provide.value('CraggyIslandService', craggyIslandService);
    });
    inject(function(_$compile_, _$rootScope_, _$q_) {
      $compile = _$compile_;
      $rootScope = _$rootScope_;
      $q = _$q_;
    });
  });

  describe('craggy island', function() {
    var element;
    var craggyIslandService;
    beforeEach(inject(function(CraggyIslandService, $q) {
      craggyIslandService = CraggyIslandService;
      craggyIslandService.sayHello = function() {
        var deferred = $q.defer();
        deferred.resolve("Get the feck off our island");
        return deferred.promise;
      };
      spyOn(craggyIslandService, 'sayHello').and.callThrough();

      element = $compile('<craggy-island></craggy-island>')($rootScope);
      $rootScope.$digest();
    }));
    it('should contain correct header (using jqLite: best practice is probably jQuery for tests)', function() {
      expect(element
        .find('h1')
        .find('span').text())
        .toBe("Get the feck off our island you big eejit!");
    });
    it ('spies on the sayHello call', function() {
      expect(craggyIslandService.sayHello).toHaveBeenCalled();
    });
    it('should contain an image of the tourist board', function() {
      expect(element.find('img').length).toBe(1);
      expect(element.find('img').attr('src')).toBe('img/tourist_office.jpg');
      expect(element.find('img').attr('alt')).toBe('tourist office');
    });
    it('should contain a subdirective', function() {
      expect(element.find('directions').length).toBe(1);
    });

    it('should contain directions directive', function() {
      expect(element.find('directions').length).toBe(1);
    });

    it('should contain inhabitants directive', function() {
      expect(element.find('craggy-island-inhabitants').length).toBe(1);
    });
    it('should contain a find-accommodation component', function() {
      expect(element.find('craggy-island-find-accommodation').length).toBe(1);
    });

    describe('directions [to craggy island]', function() {
      var directionsControl;
      var controller;
      beforeEach(function() {
        craggyIslandService.getDirections = function() {
          var deferred = $q.defer();
          deferred.resolve("Don't bother your arse");
          return deferred.promise;
        };
        spyOn(craggyIslandService, 'getDirections').and.callThrough();

        directionsControl = element.find('directions');
        controller = directionsControl.controller('directions');
        spyOn(controller, 'showDirections').and.callThrough();
      });
      afterEach(function() {
        craggyIslandService.getDirections.calls.reset();
      });

      it('contains a warning message', function() {
        expect(directionsControl.find('span.warning').text()).toBe("As a general rule, if you're going away from the island, you're going in the right direction");
      });

      it('contains a directions button', function() {
        expect(directionsControl.find('button').text()).toBe("Get Directions");
      });

      it('calls the controller#showDirections method when pressed', function() {
        var button = directionsControl.find('button');
        button.triggerHandler('click');
        expect(controller.showDirections).toHaveBeenCalled();
      });

      it('calls service#getDirections in response to controller#showDirections', function() {
        controller.showDirections();
        expect(craggyIslandService.getDirections).toHaveBeenCalled();
      });
      it('does not expose directions prior to any call to showDirections', function() {
        expect(controller.directions).not.toBeDefined();
      });
      it('does expose directions subsequent to a call to showDirections', function() {
        controller.showDirections();
        $rootScope.$digest();
        expect(controller.directions).toBe("Don't bother your arse");
      });
      it('toggles directions display on subsequent button press', function() {
        controller.showDirections();
        $rootScope.$digest();
        controller.showDirections();
        $rootScope.$digest();
        expect(controller.directions).toBeUndefined();
      });
      it('does not display directions when the controller is not providing any', function() {
        expect(directionsControl.find('div.directions span.directions').length).toBe(0);
      });
      it('does display directions when the controller is providing them', function() {
        controller.directions = "Don't bother your arse";
        $rootScope.$digest();
        expect(directionsControl.find('div.directions span.directions').length).toBe(1);
        expect(directionsControl.find('div.directions span.directions').text()).toBe("Don't bother your arse");
      });
    });
    describe('the inhabitants', function() {
      it('contains a header', function() {
        expect(element.find('h2').text()).toContain("Meet the inhabitants of craggy island");
      });
      it('contains Fr Ted', function() {
        expect(element.find('craggy-island-inhabitants ul li').text()).toContain("Fr Ted");
      });
      it('contains Fr Ted', function() {
        expect(element.find('craggy-island-inhabitants ul li').text()).toContain("Fr Dougal");
      });
    });
    describe('the accommodation component', function() {
      var accommodationComponent;
      var controller;
      beforeEach(function() {
        accommodationComponent = element.find('craggy-island-find-accommodation');
        controller = accommodationComponent.controller('craggy-island-find-accommodation');
      });
      it('has correct title', function() {
        expect(accommodationComponent.find('h2').text()).toBe("Find Accommodation: Sure what's not to like?");
      });
      it('has a controller', function() {
        expect(controller).toBeDefined();
      });
      it('contains a form', function() {
        expect(accommodationComponent.find('form').length).toBe(1);
      });
      describe('the accommodation form', function() {
        var accommodationForm;
        beforeEach(function() {
          accommodationForm = accommodationComponent.find('form');
        });
        it('has an input for name', function() {
          expect(accommodationForm.find("input[type='text'][name='name']").length).toBe(1);
        });
      });
    });

    it('should display introduction message even when there is a service error', function() {
      var craggyIslandService;
      inject(function(CraggyIslandService, $q) {
        craggyIslandService = CraggyIslandService;
        craggyIslandService.sayHello = function() {
          var deferred = $q.defer();
          deferred.reject("its broken");
          return deferred.promise;
        };
        spyOn(craggyIslandService, 'sayHello').and.callThrough();

        var element = $compile('<craggy-island></craggy-island>')($rootScope);
        $rootScope.$digest();
        expect(element.find('H1').find('span').text()).toBe("Careful now you big eejit!");
      });
    });
  });
});<commit_msg>Added some input to the form under test<commit_after>'use strict';

describe('myApp.craggy_island module', function() {
  var $compile;
  var $rootScope;
  var $q;
  var craggyIslandService;

  beforeEach(module('app.templates'));
  beforeEach(function() {
    craggyIslandService = jasmine.createSpyObj('craggyIslandService', ['sayHello', 'getDrink', 'getDirections']);
    craggyIslandService.getDrink.and.callThrough();
    craggyIslandService.getDirections.and.callThrough();
    module('myApp.hello', function($provide) {
      $provide.value('CraggyIslandService', craggyIslandService);
    });
    inject(function(_$compile_, _$rootScope_, _$q_) {
      $compile = _$compile_;
      $rootScope = _$rootScope_;
      $q = _$q_;
    });
  });

  describe('craggy island', function() {
    var element;
    var craggyIslandService;
    beforeEach(inject(function(CraggyIslandService, $q) {
      craggyIslandService = CraggyIslandService;
      craggyIslandService.sayHello = function() {
        var deferred = $q.defer();
        deferred.resolve("Get the feck off our island");
        return deferred.promise;
      };
      spyOn(craggyIslandService, 'sayHello').and.callThrough();

      element = $compile('<craggy-island></craggy-island>')($rootScope);
      $rootScope.$digest();
    }));
    it('should contain correct header (using jqLite: best practice is probably jQuery for tests)', function() {
      expect(element
        .find('h1')
        .find('span').text())
        .toBe("Get the feck off our island you big eejit!");
    });
    it ('spies on the sayHello call', function() {
      expect(craggyIslandService.sayHello).toHaveBeenCalled();
    });
    it('should contain an image of the tourist board', function() {
      expect(element.find('img').length).toBe(1);
      expect(element.find('img').attr('src')).toBe('img/tourist_office.jpg');
      expect(element.find('img').attr('alt')).toBe('tourist office');
    });
    it('should contain a subdirective', function() {
      expect(element.find('directions').length).toBe(1);
    });

    it('should contain directions directive', function() {
      expect(element.find('directions').length).toBe(1);
    });

    it('should contain inhabitants directive', function() {
      expect(element.find('craggy-island-inhabitants').length).toBe(1);
    });
    it('should contain a find-accommodation component', function() {
      expect(element.find('craggy-island-find-accommodation').length).toBe(1);
    });

    describe('directions [to craggy island]', function() {
      var directionsControl;
      var controller;
      beforeEach(function() {
        craggyIslandService.getDirections = function() {
          var deferred = $q.defer();
          deferred.resolve("Don't bother your arse");
          return deferred.promise;
        };
        spyOn(craggyIslandService, 'getDirections').and.callThrough();

        directionsControl = element.find('directions');
        controller = directionsControl.controller('directions');
        spyOn(controller, 'showDirections').and.callThrough();
      });
      afterEach(function() {
        craggyIslandService.getDirections.calls.reset();
      });

      it('contains a warning message', function() {
        expect(directionsControl.find('span.warning').text()).toBe("As a general rule, if you're going away from the island, you're going in the right direction");
      });

      it('contains a directions button', function() {
        expect(directionsControl.find('button').text()).toBe("Get Directions");
      });

      it('calls the controller#showDirections method when pressed', function() {
        var button = directionsControl.find('button');
        button.triggerHandler('click');
        expect(controller.showDirections).toHaveBeenCalled();
      });

      it('calls service#getDirections in response to controller#showDirections', function() {
        controller.showDirections();
        expect(craggyIslandService.getDirections).toHaveBeenCalled();
      });
      it('does not expose directions prior to any call to showDirections', function() {
        expect(controller.directions).not.toBeDefined();
      });
      it('does expose directions subsequent to a call to showDirections', function() {
        controller.showDirections();
        $rootScope.$digest();
        expect(controller.directions).toBe("Don't bother your arse");
      });
      it('toggles directions display on subsequent button press', function() {
        controller.showDirections();
        $rootScope.$digest();
        controller.showDirections();
        $rootScope.$digest();
        expect(controller.directions).toBeUndefined();
      });
      it('does not display directions when the controller is not providing any', function() {
        expect(directionsControl.find('div.directions span.directions').length).toBe(0);
      });
      it('does display directions when the controller is providing them', function() {
        controller.directions = "Don't bother your arse";
        $rootScope.$digest();
        expect(directionsControl.find('div.directions span.directions').length).toBe(1);
        expect(directionsControl.find('div.directions span.directions').text()).toBe("Don't bother your arse");
      });
    });
    describe('the inhabitants', function() {
      it('contains a header', function() {
        expect(element.find('h2').text()).toContain("Meet the inhabitants of craggy island");
      });
      it('contains Fr Ted', function() {
        expect(element.find('craggy-island-inhabitants ul li').text()).toContain("Fr Ted");
      });
      it('contains Fr Ted', function() {
        expect(element.find('craggy-island-inhabitants ul li').text()).toContain("Fr Dougal");
      });
    });
    describe('the accommodation component', function() {
      var accommodationComponent;
      var controller;
      beforeEach(function() {
        accommodationComponent = element.find('craggy-island-find-accommodation');
        controller = accommodationComponent.controller('craggy-island-find-accommodation');
      });
      it('has correct title', function() {
        expect(accommodationComponent.find('h2').text()).toBe("Find Accommodation: Sure what's not to like?");
      });
      it('has a controller', function() {
        expect(controller).toBeDefined();
      });
      it('contains a form', function() {
        expect(accommodationComponent.find('form').length).toBe(1);
      });
      describe('the accommodation form', function() {
        var accommodationForm;
        beforeEach(function() {
          accommodationForm = accommodationComponent.find('form');
        });
        it('has an input for name', function() {
          expect(accommodationForm.find("input[type='text'][name='name']").length).toBe(1);
        });
        it('accepts input', function() {
          var nameInput = accommodationForm.find("input[type='text'][name='name']")
          nameInput.val('dora bunner').trigger('input');
          expect(controller.name).toBe("Mrs Blackley");
        });
      });
    });

    it('should display introduction message even when there is a service error', function() {
      var craggyIslandService;
      inject(function(CraggyIslandService, $q) {
        craggyIslandService = CraggyIslandService;
        craggyIslandService.sayHello = function() {
          var deferred = $q.defer();
          deferred.reject("its broken");
          return deferred.promise;
        };
        spyOn(craggyIslandService, 'sayHello').and.callThrough();

        var element = $compile('<craggy-island></craggy-island>')($rootScope);
        $rootScope.$digest();
        expect(element.find('H1').find('span').text()).toBe("Careful now you big eejit!");
      });
    });
  });
});<|endoftext|>