<commit_before>/**
  @module ember-jsonapi-resources
  @submodule resource
**/

import Ember from 'ember';
import { pluralize, singularize } from 'ember-inflector';

/**
  A Resource class to create JSON API resource objects

  See <http://jsonapi.org/format/#document-resource-objects>

  @class Resource
  @requires Ember.Inflector
  @static
*/
const Resource = Ember.Object.extend({

  /**
    The service object for the entity (adapter with cache and serializer)

    @property service
    @type Object
    @required
  */
  service: null,

  /**
    Extending Prototypes Must define a `type` value for the entity, e.g. `posts`

    @property type
    @type String
    @required
  */
  type: null,

  /**
    Persisted resource ID value

    @property id
    @type String
    @required
  */
  id: null,

  /**
    An optional `attributes` property of for a JSON API Resource object, setup in create()

    This object will keep the values from the response object and may be mutable
    Use this as a refence for creating computed properties

    For example the `attr()` helper sets up a properties based on this content

    @protected
    @property attributes
    @type Object
  */
  attributes: null,

  /**
    An optional `relationships` property of for a JSON API Resource object, setup in create()

    @protected
    @property relationships
    @type Object
  */
  relationships: null,

  /**
    An optional `links` property of for a JSON API Resource object, setup in create()

    @protected
    @property links
    @type Object
  */
  links: null,

  /**
    An optional property of for a JSON API Resource object, setup in create()

    @protected
    @property meta
    @type Object
  */
  meta: null,

  /**
    Hash of attributes for changed/previous values

    @private
    @property _attributes
    @type Object
  */
  _attributes: null,

  /**
    Flag for new instance, e.g. not peristed

    @property isNew
    @type Boolean
  */
  isNew: false,

  /**
    Custom `toString` method used for clarity that the instance is a JSON API Resource kind of object

    @method toString
  */
  toString() {
    return Ember.String.fmt("[JSONAPIResource|%@:%@]", this.get('type'), this.get('id'));
  },

  /**
    Adds related links object on the relationship hash

    @method addRelationship
    @param {String} related - resource name
    @param {String} id
  */
  addRelationship(related, id) {
    const key = ['relationships', related, 'data'].join('.');
    let data = this.get(key);
    const type = pluralize(related);
    const linkage = { type: type, id: id };
    if (Array.isArray(data)) {
      data.pushObject(linkage);
    } else {
      data = linkage;
    }
    return this.set(key, data);
  },

  /**
    Remove related links object on the relationship hash to have `null` data

    @method removeRelationship
    @param {String} related - resource name
    @param {String} id
  */
  removeRelationship(related, id) {
    let relation = this.get('relationships.' + related);
    if (Array.isArray(relation.data)) {
      for (let i = 0; i < relation.data.length; i++) {
        if (relation.data[i].id === id) {
          relation.data.splice(i, 1);
          break;
        }
      }
    } else if (typeof relation === 'object') {
      relation.data = null;
    }
  },

  /**
    @method changedAttributes
    @return {Object} the changed attributes
  */
  changedAttributes: Ember.computed('attributes', {
    get: function () {
      const attrs = {};
      for (let key in this._attributes) {
        if (this._attributes.hasOwnProperty(key)) {
          if (this._attributes[key].changed !== this._attributes[key].previous) {
            attrs[key] = this._attributes[key].changed;
          }
        }
      }
      return attrs;
    }
  }).volatile(),

  /**
    @method previousAttributes
    @return {Object} the previous attributes
  */
  previousAttributes: Ember.computed('attributes', {
    get: function () {
      const attrs = {};
      for (let key in this._attributes) {
        if (this._attributes.hasOwnProperty(key)) {
          if (this._attributes[key].changed !== this._attributes[key].previous) {
            attrs[key] = this._attributes[key].previous;
          }
        }
      }
      return attrs;
    }
  }).volatile(),

  /**
    Initialize events to communicate with the service object, listen for `didUpdateResource`

    @method initEvents
  */
  initEvents: Ember.on('init', function () {
    const service = this.get('service');
    if (service) {
      service.on('didUpdateResource', this, this.didUpdateResource);
    }
  }),

  /**
    Handler for `didUpdateResource` event, resets private _attributes used for changed/previous tracking

    @method didUpdateResource
  */
  didUpdateResource(json) {
    if (this.get('id') !== json.id) { return; }
    for (let attr in this._attributes) {
      if (this._attributes.hasOwnProperty(attr)) {
        delete this._attributes[attr];
      }
    }
  },

  /**
    A local cache duration, to minimize duplicate fetch requests

    @property cacheDuration
    @type Number
  */
  cacheDuration: /* minutes */ 7 * /* seconds */ 60 * /* milliseconds */ 1000,

  /**
    @property isCacheExpired
    @type Boolean
  */
  isCacheExpired: Ember.computed('meta.timeStamps.local', 'cacheDuration', function () {
    const localTime = this.get('meta.timeStamps.local');
    const expiresTime = localTime + this.get('cacheDuration');
    return (localTime) ? Date.now() >= expiresTime : false;
  })
});

Resource.reopenClass({
  /**
    To protect the JSON API Resource properties for attributes, links and
    relationships these objects are setup during create(). This has to be
    defined since the attr() helper needs to have new objects for each instance,
    to project from keeping a reference on the prototype.

    The create method should only be called after looking up a factory from the
    container, for example in a route's model hook:

    ```
    model() {
      return this.container.lookupFactory('model:posts').create({
        attributes: {
          title: 'The JSON API 1.0 Spec Rocks!'
        }
      });
    }
    ```

    The create method uses the container to lookup the factory's prototype and
    find the computed properties used for relations to setup the relationships
    for the Resource instance you create. Calling Resource#create without using
    the factory lookup will result in an instance without a reference to the
    application's container and you will have to manually setup the relationships
    object prior to adding a relationship.

    @method create
    @returns {Resource} instance with protected objects:
      `attributes`, `links` and `relationships`
  */
  create(properties) {
    const prototype = {};
    const attrs = Ember.String.w('_attributes attributes links meta relationships');
    for (let i = 0; i < attrs.length; i++) {
      prototype[attrs[i]] = {};
    }
    const instance = this._super(prototype);
    if (properties) {
      instance.setProperties(properties);
    }
    let type = instance.get('type');
    let msg = (type) ? Ember.String.capitalize(singularize(type)) : 'Resource';
    if (!type) {
      Ember.Logger.warn(msg + '#create called, instead you should first use ' + msg + '.extend({type:"entity"})');
    }
    let factory = 'model:' + type;
    if (instance.container) {
      factory = instance.container.lookupFactory(factory);
      let proto = factory.proto();
      factory.eachComputedProperty(function(prop) {
        if (proto[prop] && proto[prop]._meta && typeof proto[prop]._meta === 'object') {
          if (proto[prop]._meta.kind === 'hasOne') {
            setupRelationship.call(instance, prop);
          } else if (proto[prop]._meta.kind === 'hasMany') {
            setupRelationship.call(instance, prop, Ember.A([]));
          }
        }
      });
    } else {
      msg += '#create should only be called from a container lookup (relationships not setup) ';
      msg += 'use this.container.lookupFactory("' + factory + '").create() instead.';
      Ember.Logger.warn(msg);
    }
    return instance;
  }
});

export default Resource;

/**
  Helper to setup computed property for resource attributes

  @method attr
*/
export function attr(type, mutable = true) {
  const _mutable = mutable;
  return Ember.computed('attributes', {
    get: function (key) {
      return this.get('attributes.' + key);
    },

    set: function (key, value) {
      const lastValue = this.get('attributes.' + key);
      if (!_mutable) { return lastValue; }
      if (value === lastValue) { return value; }
      this.set('attributes.' + key, value);
      if (!this.get('isNew')) {
        this._attributes[key] = this._attributes[key] || {};
        this._attributes[key].changed = value;
        this._attributes[key].previous = lastValue;
        const service = this.get('service');
        if (service) {
          service.trigger('attributeChanged', this);
        }
      }
      return this.get('attributes.' + key);
    }
  });
}

/**
  Mixin for creating promise proxy objects for related resources

  @class RelatedProxyUtil
  @static
*/
const RelatedProxyUtil = Ember.Object.extend({

  /**
    Checks for required `relationship` property

    @method init
  */
  init: function () {
    this._super();
    if (typeof this.get('relationship') !== 'string') {
      throw new Error('RelatedProxyUtil#init expects `relationship` property to exist.');
    }
    return this;
  },

  /**
    The name of the relationship

    @property resource
    @type String
    @required
  */
  relationship: null,

  /**
    Proxy for the requested relation, resolves w/ content from fulfilled promise

    @method createProxy
    @param {Resource} resource
    @param {Ember.ObjectProxy|Ember.ArrayProxy} proxyFactory
    @return {PromiseProxy} proxy
  */
  createProxy: function (resource, proxyFactory) {
    const relation = this.get('relationship');
    const url = this.proxyUrl(resource, relation);
    const service = resource.container.lookup('service:' + pluralize(relation));
    let promise = this.promiseFromCache(resource, relation, service);
    promise = promise || service.findRelated(relation, url);
    let proxy = proxyFactory.extend(Ember.PromiseProxyMixin, {
      'promise': promise, 'type': relation
    });
    proxy = proxy.create();
    proxy.then(
      function (resources) {
        proxy.set('content', resources);
      },
      function (error) {
        console.error(error);
        throw error;
      }
    );
    return proxy;
  },

  /**
    Proxy url to fetch for the resource's relation

    @method proxyUrl
    @param {Resource} resource
    @param {String} relation
    @return {PromiseProxy} proxy
  */
  proxyUrl(resource, relation) {
    const related = linksPath(relation);
    const url = resource.get(related);
    if (typeof url !== 'string') {
      throw new Error('RelatedProxyUtil#_proxyUrl expects `model.'+ related +'` property to exist.');
    }
    return url;
  },

  /**
    Lookup relation from service cache and pomisify result

    @method promiseFromCache
    @param {Resource} resource
    @param {String} relation
    @param {Object} service
    @return {Promise|null}
  */
  promiseFromCache(resource, relation, service) {
    let data = resource.get('relationships.' + relation + '.data');
    if (!data) { return; }
    let content = Ember.A([]), found;
    if (Array.isArray(data)) {
      for (let i = 0; i < data.length; i++) {
        found = this.serviceCacheLookup(service, data[i]);
        if (found) {
          content.push(found);
        }
      }
      content = (data.length && data.length === content.length) ? content : null;
    } else {
      content = this.serviceCacheLookup(service, data);
    }
    return (content) ? Ember.RSVP.Promise.resolve(content) : null;
  },

  /**
    Lookup data in service cache

    @method serviceCacheLookup
    @param {Object} service
    @param {Object} data
    @return {Resource|undefined}
  */
  serviceCacheLookup(service, data) {
    return (typeof data === 'object' && data.id) ? service.cacheLookup(data.id) : undefined;
  }
});

function linksPath(relation) {
  return ['relationships', relation, 'links', 'related'].join('.');
}

/**
  Helper to setup a has one relationship to another resource

  @method hasOne
  @param {String} relation
*/
export function hasOne(relation) {
  const util = RelatedProxyUtil.create({'relationship': relation});
  const path = linksPath(relation);
  return Ember.computed(path, function () {
    return util.createProxy(this, Ember.ObjectProxy);
  }).meta({relation: relation, kind: 'hasOne'});
}

/**
  Helper to setup a has many relationship to another resource

  @method hasMany
  @param {String} relation
*/
export function hasMany(relation) {
  const util = RelatedProxyUtil.create({'relationship': relation});
  const path = linksPath(relation);
  return Ember.computed(path, function () {
    return util.createProxy(this, Ember.ArrayProxy);
  }).meta({relation: relation, kind: 'hasMany'});
}

function setupRelationship(relation, data = null) {
  if (!this.relationships[relation]) {
    this.relationships[relation] = { links: {}, data: data };
  }
  if (!this.relationships[relation].links) {
    this.relationships[relation].links = {};
  }
  if (!this.relationships[relation].data) {
    this.relationships[relation].data = data;
  }
}
<commit_msg>Add assertion for dasherized keys in resource<commit_after>/**
  @module ember-jsonapi-resources
  @submodule resource
**/

import Ember from 'ember';
import { pluralize, singularize } from 'ember-inflector';

/**
  A Resource class to create JSON API resource objects

  See <http://jsonapi.org/format/#document-resource-objects>

  @class Resource
  @requires Ember.Inflector
  @static
*/
const Resource = Ember.Object.extend({

  /**
    The service object for the entity (adapter with cache and serializer)

    @property service
    @type Object
    @required
  */
  service: null,

  /**
    Extending Prototypes Must define a `type` value for the entity, e.g. `posts`

    @property type
    @type String
    @required
  */
  type: null,

  /**
    Persisted resource ID value

    @property id
    @type String
    @required
  */
  id: null,

  /**
    An optional `attributes` property of for a JSON API Resource object, setup in create()

    This object will keep the values from the response object and may be mutable
    Use this as a refence for creating computed properties

    For example the `attr()` helper sets up a properties based on this content

    @protected
    @property attributes
    @type Object
  */
  attributes: null,

  /**
    An optional `relationships` property of for a JSON API Resource object, setup in create()

    @protected
    @property relationships
    @type Object
  */
  relationships: null,

  /**
    An optional `links` property of for a JSON API Resource object, setup in create()

    @protected
    @property links
    @type Object
  */
  links: null,

  /**
    An optional property of for a JSON API Resource object, setup in create()

    @protected
    @property meta
    @type Object
  */
  meta: null,

  /**
    Hash of attributes for changed/previous values

    @private
    @property _attributes
    @type Object
  */
  _attributes: null,

  /**
    Flag for new instance, e.g. not peristed

    @property isNew
    @type Boolean
  */
  isNew: false,

  /**
    Custom `toString` method used for clarity that the instance is a JSON API Resource kind of object

    @method toString
  */
  toString() {
    return Ember.String.fmt("[JSONAPIResource|%@:%@]", this.get('type'), this.get('id'));
  },

  /**
    Adds related links object on the relationship hash

    @method addRelationship
    @param {String} related - resource name
    @param {String} id
  */
  addRelationship(related, id) {
    const key = ['relationships', related, 'data'].join('.');
    let data = this.get(key);
    const type = pluralize(related);
    const linkage = { type: type, id: id };
    if (Array.isArray(data)) {
      data.pushObject(linkage);
    } else {
      data = linkage;
    }
    return this.set(key, data);
  },

  /**
    Remove related links object on the relationship hash to have `null` data

    @method removeRelationship
    @param {String} related - resource name
    @param {String} id
  */
  removeRelationship(related, id) {
    let relation = this.get('relationships.' + related);
    if (Array.isArray(relation.data)) {
      for (let i = 0; i < relation.data.length; i++) {
        if (relation.data[i].id === id) {
          relation.data.splice(i, 1);
          break;
        }
      }
    } else if (typeof relation === 'object') {
      relation.data = null;
    }
  },

  /**
    @method changedAttributes
    @return {Object} the changed attributes
  */
  changedAttributes: Ember.computed('attributes', {
    get: function () {
      const attrs = {};
      for (let key in this._attributes) {
        if (this._attributes.hasOwnProperty(key)) {
          if (this._attributes[key].changed !== this._attributes[key].previous) {
            attrs[key] = this._attributes[key].changed;
          }
        }
      }
      return attrs;
    }
  }).volatile(),

  /**
    @method previousAttributes
    @return {Object} the previous attributes
  */
  previousAttributes: Ember.computed('attributes', {
    get: function () {
      const attrs = {};
      for (let key in this._attributes) {
        if (this._attributes.hasOwnProperty(key)) {
          if (this._attributes[key].changed !== this._attributes[key].previous) {
            attrs[key] = this._attributes[key].previous;
          }
        }
      }
      return attrs;
    }
  }).volatile(),

  /**
    Initialize events to communicate with the service object, listen for `didUpdateResource`

    @method initEvents
  */
  initEvents: Ember.on('init', function () {
    const service = this.get('service');
    if (service) {
      service.on('didUpdateResource', this, this.didUpdateResource);
    }
  }),

  /**
    Handler for `didUpdateResource` event, resets private _attributes used for changed/previous tracking

    @method didUpdateResource
  */
  didUpdateResource(json) {
    if (this.get('id') !== json.id) { return; }
    for (let attr in this._attributes) {
      if (this._attributes.hasOwnProperty(attr)) {
        delete this._attributes[attr];
      }
    }
  },

  /**
    A local cache duration, to minimize duplicate fetch requests

    @property cacheDuration
    @type Number
  */
  cacheDuration: /* minutes */ 7 * /* seconds */ 60 * /* milliseconds */ 1000,

  /**
    @property isCacheExpired
    @type Boolean
  */
  isCacheExpired: Ember.computed('meta.timeStamps.local', 'cacheDuration', function () {
    const localTime = this.get('meta.timeStamps.local');
    const expiresTime = localTime + this.get('cacheDuration');
    return (localTime) ? Date.now() >= expiresTime : false;
  })
});

Resource.reopenClass({
  /**
    To protect the JSON API Resource properties for attributes, links and
    relationships these objects are setup during create(). This has to be
    defined since the attr() helper needs to have new objects for each instance,
    to project from keeping a reference on the prototype.

    The create method should only be called after looking up a factory from the
    container, for example in a route's model hook:

    ```
    model() {
      return this.container.lookupFactory('model:posts').create({
        attributes: {
          title: 'The JSON API 1.0 Spec Rocks!'
        }
      });
    }
    ```

    The create method uses the container to lookup the factory's prototype and
    find the computed properties used for relations to setup the relationships
    for the Resource instance you create. Calling Resource#create without using
    the factory lookup will result in an instance without a reference to the
    application's container and you will have to manually setup the relationships
    object prior to adding a relationship.

    @method create
    @returns {Resource} instance with protected objects:
      `attributes`, `links` and `relationships`
  */
  create(properties) {
    const prototype = {};
    const attrs = Ember.String.w('_attributes attributes links meta relationships');
    for (let i = 0; i < attrs.length; i++) {
      prototype[attrs[i]] = {};
    }
    const instance = this._super(prototype);
    if (properties) {
      instance.setProperties(properties);
    }
    let type = instance.get('type');
    let msg = (type) ? Ember.String.capitalize(singularize(type)) : 'Resource';
    if (!type) {
      Ember.Logger.warn(msg + '#create called, instead you should first use ' + msg + '.extend({type:"entity"})');
    }
    let factory = 'model:' + type;
    if (instance.container) {
      factory = instance.container.lookupFactory(factory);
      let proto = factory.proto();
      factory.eachComputedProperty(function(prop) {
        if (proto[prop] && proto[prop]._meta && typeof proto[prop]._meta === 'object') {
          if (proto[prop]._meta.kind === 'hasOne') {
            setupRelationship.call(instance, prop);
          } else if (proto[prop]._meta.kind === 'hasMany') {
            setupRelationship.call(instance, prop, Ember.A([]));
          }
        }
      });
    } else {
      msg += '#create should only be called from a container lookup (relationships not setup) ';
      msg += 'use this.container.lookupFactory("' + factory + '").create() instead.';
      Ember.Logger.warn(msg);
    }
    return instance;
  }
});

export default Resource;

/**
  Helper to setup computed property for resource attributes

  @method attr
*/
export function attr(type, mutable = true) {
  const _mutable = mutable;
  return Ember.computed('attributes', {
    get: function (key) {
      assertDasherizedAttr(key);
      return this.get('attributes.' + key);
    },

    set: function (key, value) {
      const lastValue = this.get('attributes.' + key);
      if (!_mutable) { return lastValue; }
      if (value === lastValue) { return value; }
      this.set('attributes.' + key, value);
      if (!this.get('isNew')) {
        this._attributes[key] = this._attributes[key] || {};
        this._attributes[key].changed = value;
        this._attributes[key].previous = lastValue;
        const service = this.get('service');
        if (service) {
          service.trigger('attributeChanged', this);
        }
      }
      return this.get('attributes.' + key);
    }
  });
}

function assertDasherizedAttr(name) {
  try {
    let attrName = Ember.String.dasherize(name);
    let msg = "Attributes are recommended to use dasherized names, e.g `'"+ attrName +"': attr()`";
    msg += ", instead of `"+ name +": attr()`";
    Ember.assert(msg, isDasherized(name));
  } catch(e) {
    console.warn(e.message);
  }
}

/**
  Mixin for creating promise proxy objects for related resources

  @class RelatedProxyUtil
  @static
*/
const RelatedProxyUtil = Ember.Object.extend({

  /**
    Checks for required `relationship` property

    @method init
  */
  init: function () {
    this._super();
    if (typeof this.get('relationship') !== 'string') {
      throw new Error('RelatedProxyUtil#init expects `relationship` property to exist.');
    }
    return this;
  },

  /**
    The name of the relationship

    @property resource
    @type String
    @required
  */
  relationship: null,

  /**
    Proxy for the requested relation, resolves w/ content from fulfilled promise

    @method createProxy
    @param {Resource} resource
    @param {Ember.ObjectProxy|Ember.ArrayProxy} proxyFactory
    @return {PromiseProxy} proxy
  */
  createProxy: function (resource, proxyFactory) {
    const relation = this.get('relationship');
    const url = this.proxyUrl(resource, relation);
    const service = resource.container.lookup('service:' + pluralize(relation));
    let promise = this.promiseFromCache(resource, relation, service);
    promise = promise || service.findRelated(relation, url);
    let proxy = proxyFactory.extend(Ember.PromiseProxyMixin, {
      'promise': promise, 'type': relation
    });
    proxy = proxy.create();
    proxy.then(
      function (resources) {
        proxy.set('content', resources);
      },
      function (error) {
        console.error(error);
        throw error;
      }
    );
    return proxy;
  },

  /**
    Proxy url to fetch for the resource's relation

    @method proxyUrl
    @param {Resource} resource
    @param {String} relation
    @return {PromiseProxy} proxy
  */
  proxyUrl(resource, relation) {
    const related = linksPath(relation);
    const url = resource.get(related);
    if (typeof url !== 'string') {
      throw new Error('RelatedProxyUtil#_proxyUrl expects `model.'+ related +'` property to exist.');
    }
    return url;
  },

  /**
    Lookup relation from service cache and pomisify result

    @method promiseFromCache
    @param {Resource} resource
    @param {String} relation
    @param {Object} service
    @return {Promise|null}
  */
  promiseFromCache(resource, relation, service) {
    let data = resource.get('relationships.' + relation + '.data');
    if (!data) { return; }
    let content = Ember.A([]), found;
    if (Array.isArray(data)) {
      for (let i = 0; i < data.length; i++) {
        found = this.serviceCacheLookup(service, data[i]);
        if (found) {
          content.push(found);
        }
      }
      content = (data.length && data.length === content.length) ? content : null;
    } else {
      content = this.serviceCacheLookup(service, data);
    }
    return (content) ? Ember.RSVP.Promise.resolve(content) : null;
  },

  /**
    Lookup data in service cache

    @method serviceCacheLookup
    @param {Object} service
    @param {Object} data
    @return {Resource|undefined}
  */
  serviceCacheLookup(service, data) {
    return (typeof data === 'object' && data.id) ? service.cacheLookup(data.id) : undefined;
  }
});

function linksPath(relation) {
  return ['relationships', relation, 'links', 'related'].join('.');
}

/**
  Helper to setup a has one relationship to another resource

  @method hasOne
  @param {String} relation
*/
export function hasOne(relation) {
  assertDasherizedHasOneRelation(relation);
  const util = RelatedProxyUtil.create({'relationship': relation});
  const path = linksPath(relation);
  return Ember.computed(path, function () {
    return util.createProxy(this, Ember.ObjectProxy);
  }).meta({relation: relation, kind: 'hasOne'});
}

function assertDasherizedHasOneRelation(name) {
  try {
    let relationName = Ember.String.dasherize(name);
    let msg = " are recommended to use dasherized names, e.g `hasOne('"+ relationName +"')`";
    msg += ", instead of `hasOne('"+ name +"')`";
    Ember.assert(msg, isDasherized(name));
  } catch(e) {
    console.warn(e.message);
  }
}

/**
  Helper to setup a has many relationship to another resource

  @method hasMany
  @param {String} relation
*/
export function hasMany(relation) {
  assertDasherizedHasManyRelation(relation);
  const util = RelatedProxyUtil.create({'relationship': relation});
  const path = linksPath(relation);
  return Ember.computed(path, function () {
    return util.createProxy(this, Ember.ArrayProxy);
  }).meta({relation: relation, kind: 'hasMany'});
}

function assertDasherizedHasManyRelation(name) {
  try {
    let relationName = Ember.String.dasherize(name);
    let msg = " are recommended to use dasherized names, e.g `hasMany('"+ relationName +"')`";
    msg += ", instead of `hasMany('"+ name +"')`";
    Ember.assert(msg, isDasherized(name));
  } catch(e) {
    console.warn(e.message);
  }
}

function setupRelationship(relation, data = null) {
  if (!this.relationships[relation]) {
    this.relationships[relation] = { links: {}, data: data };
  }
  if (!this.relationships[relation].links) {
    this.relationships[relation].links = {};
  }
  if (!this.relationships[relation].data) {
    this.relationships[relation].data = data;
  }
}

function isDasherized(name) {
  return (Ember.String.dasherize(name) === name);
}
<|endoftext|>


<commit_before>// This is a Controller mixin for adding methods to manage middleware creation.

// __Dependencies__
var middleware = require('../middleware');

// __Private Module Members__

// Handle variable number of arguments
function last (skip, names, values) {
  var r = {};
  var position = names.length;
  var count = Object.keys(values).filter(function (key) { return values[key] }).length - skip;

  if (count === 0) throw new Error('Too few arguments.');

  names.forEach(function (name) {
    var index = skip + count - position;
    position--;
    if (index >= skip) r[name] = values[index];
  });

  return r;
}

// Parse middleware into an array of middleware definitions for each howMany and verb
function factor (stage, options) {
  if (!stage) throw new Error('Must supply stage.');

  var factored = [];

  if (options.verbs) {
    options.verbs = options.verbs.toLowerCase();
    options.verbs.split(/\s+/).forEach(function (verb) {
      if (!/^head|get|put|post|del$/.exec(verb)) throw new Error('Unrecognized verb: ' + verb);
    });
  }

  if (options.howMany && options.howMany !== 'instance' && options.howMany !== 'collection') {
    throw new Error('Unrecognized howMany: ' + options.howMany);
  }

  // Middleware function or array
  if (Array.isArray(options.middleware) || typeof options.middleware === 'function') {
    if (options.howMany !== 'collection') factored.push({ stage: stage, howMany: 'instance', verbs: options.verbs, middleware: options.middleware });
    if (options.howMany !== 'instance') factored.push({ stage: stage, howMany: 'collection', verbs: options.verbs, middleware: options.middleware });
    return factored;
  }

  // Middleware hash keyed on instance/collection, then verb
  if (options.howMany) throw new Error('Specified instance/collection twice.');
  if (options.verbs) throw new Error('Specified verbs twice.');

  Object.keys(options.middleware).forEach(function (howManyKey) {
    Object.keys(options.middleware[howManyKey]).map(function (verb) {
      factored.push({
        stage: stage,
        howMany: howManyKey,
        verbs: verb,
        middleware: options.middleware[howManyKey][verb]
      });
    });
  });

  return factored;
}

// __Module Definition__
var mixin = module.exports = function () {

  // __Private Instance Members__

  var controller = this;
  // Flags whether the custom middleware has been activated
  var initialized = false;
  // A hash for storing user middleware
  var custom = {
    "request":
    { "instance": { "head": [], "get": [], "post": [], "put": [], "del": [] },
     "collection": { "head": [], "get": [], "post": [], "put": [], "del": [] }
    },
    "query":
    { "instance": { "head": [], "get": [], "post": [], "put": [], "del": [] },
     "collection": { "head": [], "get": [], "post": [], "put": [], "del": [] }
    },
    "documents":
    { "instance": { "head": [], "get": [], "post": [], "put": [], "del": [] },
     "collection": { "head": [], "get": [], "post": [], "put": [], "del": [] }
    }
  };

  // A method used to register user middleware to be activated during intialization.
  function register (stage, howMany, verbs, middleware) {
    if (initialized) {
      throw new Error("Can't add middleware after the controller has been activated.");
    }

    var options = last(1, ['howMany', 'verbs', 'middleware'], arguments);

    // Prevent explicitly setting query-stage POST middleware.  Implicitly adding
    // this middleware is ignored.
    if (stage === 'query' && options.verbs && options.verbs.indexOf('post') !== -1) {
      throw new Error('Query stage not executed for POST.');
    }

    factor(stage, options).forEach(function (definition) {
      var verbs = definition.verbs || 'head get post put del';
      verbs.split(' ').forEach(function (verb) {
        if (controller.get(verb) === false) return;
        custom[definition.stage][definition.howMany][verb] = custom[definition.stage][definition.howMany][verb].concat(definition.middleware);
      });
    });
  }

  // Core function for activating middleware
  function _activate (factored, override) {
    if (initialized) throw new Error("Can't activate middleware after the controller has been activated.");

    factored.forEach(function (definition) {
      var verbs = definition.verbs || 'head get post put del';

      verbs.split(' ').forEach(function (verb) {
        if (!override && controller.get(verb) === false) return;

        var path;

        if (definition.howMany === 'instance') path = controller.get('basePathWithId');
        else if (definition.howMany === 'collection') path = controller.get('basePath');
        else throw new Error('Unrecognized howMany.');

        controller[verb](path, definition.middleware);
      });
    });
  }

  // A method used to activate baucis middleware & user middleware that was
  // previously registered.
  function activate (stage, howMany, verbs, middleware) {
    var options = last(1, ['howMany', 'verbs', 'middleware'], arguments);
    _activate(factor(stage, options), false);
  }

  // A method used to activate middleware overriding any verb options
  function activateOverride (stage, howMany, verbs, middleware) {
    var options = last(1, ['howMany', 'verbs', 'middleware'], arguments);
    _activate(factor(stage, options), true);
  }

  // __Public Instance Methods__

  // A method used to register request-stage middleware.
  controller.request = function (howMany, verbs, middleware) {
    register('request', howMany, verbs, middleware);
    return controller;
  };

  // A method used to register query-stage middleware.
  controller.query = function (howMany, verbs, middleware) {
    register('query', howMany, verbs, middleware);
    return controller;
  };

  // A method used to register document-stage middleware.
  controller.documents = function (howMany, verbs, middleware) {
    register('documents', howMany, verbs, middleware);
    return controller;
  };

  // A method used to intialize the controller and activate user middleware.  It
  // may be called multiple times, but will trigger intialization only once.
  controller.initialize = function () {
    if (initialized) return controller;

    // __Request-Stage Middleware__

    // Activate middleware to check for deprecated features
    activate('request', middleware.configure.deprecated);
    // Activate middleware that sets the Allow & Accept headers
    activateOverride('request', [ middleware.headers.allow, middleware.headers.accept ]);
    // Activate middleware that checks for correct Mongo _ids when applicable
    activate('request', middleware.configure.checkId);
    // Activate middleware that checks for disabled HTTP methods
    activateOverride('request', middleware.configure.checkMethod);
    // Activate middleware to set request.baucis.conditions for find/remove
    activate('request', 'collection', 'head get del', middleware.configure.conditions);
    // Also activate conditions middleware for update
    activate('request', 'instance', 'put', middleware.configure.conditions);
    // Activate middleware to set request.baucis.count when query is present
    activate('request', 'get', middleware.configure.count);
    // Next, activate the request-stage user middleware.
    activate('request', custom['request']);
    // Activate middleware to build the query (except for POST requests).
    activate('request', middleware.query);

    // __Query-Stage Middleware__
    // The query will have been created (except for POST, which doesn't use a
    // find or remove query).

    // Activate middleware to handle controller and query options.
    activate('query', [ middleware.configure.controller, middleware.configure.query ]);

    // Delete any query instance POST middleware that was added implicitly.
    custom.query.instance.post = [];
    // Delete any query collection PUT middleware that was added implicitly.
    custom.query.collection.put = [];

    // Activate user middleware for the query-stage
    activate('query', custom['query']);

    // Activate middleware to execute the query:

    // Get the count for HEAD requests.
    activate('query', 'head', middleware.exec.count);
    // Execute the find or remove query for GET and DELETE.
    activate('query', 'get del', middleware.exec.exec);
    // Create the documents for a POST request.
    activate('query', 'collection', 'post', middleware.exec.create);
    // Update the documents specified for a PUT request.
    activate('query', 'instance', 'put', middleware.exec.update);

    // Activate some middleware that will set the Link header when that feature
    // is enabled.  (This must come after exec or else the count is
    // returned for all subsequqent executions of the query.)
    if (controller.get('relations') === true) {
      activate('query', 'instance', 'head get post put del', middleware.headers.link);
      activate('query', 'collection', 'head get post put del', middleware.headers.linkCollection);
    }

    // __Document-Stage Middleware__

    // Activate the middleware that sets the `Last-Modified` header when appropriate.
    activate('documents', middleware.documents.lastModified);
    // Activate the the document-stage user middleware.
    activate('documents', custom['documents']);
    // Activate the middleware that sends the resulting document(s) or count.
    activate('documents', middleware.documents.send);

    delete custom;
    initialized = true;
    return controller;
  };
};
<commit_msg>Subcontroller problem workaround<commit_after>// This is a Controller mixin for adding methods to manage middleware creation.

// __Dependencies__
var middleware = require('../middleware');

// __Private Module Members__

// Handle variable number of arguments
function last (skip, names, values) {
  var r = {};
  var position = names.length;
  var count = Object.keys(values).filter(function (key) { return values[key] }).length - skip;

  if (count === 0) throw new Error('Too few arguments.');

  names.forEach(function (name) {
    var index = skip + count - position;
    position--;
    if (index >= skip) r[name] = values[index];
  });

  return r;
}

// Parse middleware into an array of middleware definitions for each howMany and verb
function factor (stage, options) {
  if (!stage) throw new Error('Must supply stage.');

  var factored = [];

  if (options.verbs) {
    options.verbs = options.verbs.toLowerCase();
    options.verbs.split(/\s+/).forEach(function (verb) {
      if (!/^head|get|put|post|del$/.exec(verb)) throw new Error('Unrecognized verb: ' + verb);
    });
  }

  if (options.howMany && options.howMany !== 'instance' && options.howMany !== 'collection') {
    throw new Error('Unrecognized howMany: ' + options.howMany);
  }

  // Middleware function or array
  if (Array.isArray(options.middleware) || typeof options.middleware === 'function') {
    if (options.howMany !== 'collection') factored.push({ stage: stage, howMany: 'instance', verbs: options.verbs, middleware: options.middleware });
    if (options.howMany !== 'instance') factored.push({ stage: stage, howMany: 'collection', verbs: options.verbs, middleware: options.middleware });
    return factored;
  }

  // Middleware hash keyed on instance/collection, then verb
  if (options.howMany) throw new Error('Specified instance/collection twice.');
  if (options.verbs) throw new Error('Specified verbs twice.');

  Object.keys(options.middleware).forEach(function (howManyKey) {
    Object.keys(options.middleware[howManyKey]).map(function (verb) {
      factored.push({
        stage: stage,
        howMany: howManyKey,
        verbs: verb,
        middleware: options.middleware[howManyKey][verb]
      });
    });
  });

  return factored;
}

// __Module Definition__
var mixin = module.exports = function () {

  // __Private Instance Members__

  var controller = this;
  // Flags whether the custom middleware has been activated
  var initialized = false;
  // A hash for storing user middleware
  var custom = {
    "request":
    { "instance": { "head": [], "get": [], "post": [], "put": [], "del": [] },
     "collection": { "head": [], "get": [], "post": [], "put": [], "del": [] }
    },
    "query":
    { "instance": { "head": [], "get": [], "post": [], "put": [], "del": [] },
     "collection": { "head": [], "get": [], "post": [], "put": [], "del": [] }
    },
    "documents":
    { "instance": { "head": [], "get": [], "post": [], "put": [], "del": [] },
     "collection": { "head": [], "get": [], "post": [], "put": [], "del": [] }
    }
  };

  // A method used to register user middleware to be activated during intialization.
  function register (stage, howMany, verbs, middleware) {
    if (initialized) {
      throw new Error("Can't add middleware after the controller has been activated.");
    }

    var options = last(1, ['howMany', 'verbs', 'middleware'], arguments);

    // Prevent explicitly setting query-stage POST middleware.  Implicitly adding
    // this middleware is ignored.
    if (stage === 'query' && options.verbs && options.verbs.indexOf('post') !== -1) {
      throw new Error('Query stage not executed for POST.');
    }

    factor(stage, options).forEach(function (definition) {
      var verbs = definition.verbs || 'head get post put del';
      verbs.split(' ').forEach(function (verb) {
        if (controller.get(verb) === false) return;
        custom[definition.stage][definition.howMany][verb] = custom[definition.stage][definition.howMany][verb].concat(definition.middleware);
      });
    });
  }

  // Core function for activating middleware
  function _activate (factored, override) {
    if (initialized) throw new Error("Can't activate middleware after the controller has been activated.");

    factored.forEach(function (definition) {
      var verbs = definition.verbs || 'head get post put del';

      verbs.split(' ').forEach(function (verb) {
        if (!override && controller.get(verb) === false) return;

        var path;

        if (definition.howMany === 'instance') path = controller.get('basePathWithId');
        else if (definition.howMany === 'collection') path = controller.get('basePath');
        else throw new Error('Unrecognized howMany.');

        controller[verb](path, definition.middleware);
      });
    });
  }

  // A method used to activate baucis middleware & user middleware that was
  // previously registered.
  function activate (stage, howMany, verbs, middleware) {
    var options = last(1, ['howMany', 'verbs', 'middleware'], arguments);
    _activate(factor(stage, options), false);
  }

  // A method used to activate middleware overriding any verb options
  function activateOverride (stage, howMany, verbs, middleware) {
    var options = last(1, ['howMany', 'verbs', 'middleware'], arguments);
    _activate(factor(stage, options), true);
  }

  // __Public Instance Methods__

  // A method used to register request-stage middleware.
  controller.request = function (howMany, verbs, middleware) {
    register('request', howMany, verbs, middleware);
    return controller;
  };

  // A method used to register query-stage middleware.
  controller.query = function (howMany, verbs, middleware) {
    register('query', howMany, verbs, middleware);
    return controller;
  };

  // A method used to register document-stage middleware.
  controller.documents = function (howMany, verbs, middleware) {
    register('documents', howMany, verbs, middleware);
    return controller;
  };

  // A method used to intialize the controller and activate user middleware.  It
  // may be called multiple times, but will trigger intialization only once.
  controller.initialize = function () {
    if (initialized) return controller;

     // Subcontroller problem workaround, see https://github.com/wprl/baucis/issues/86
    var alwaysSetController = function (request, response, next){
        request.baucis.controller = controller;
        next();
    }
    activate('request', alwaysSetController);

    // __Request-Stage Middleware__

    // Activate middleware to check for deprecated features
    activate('request', middleware.configure.deprecated);
    // Activate middleware that sets the Allow & Accept headers
    activateOverride('request', [ middleware.headers.allow, middleware.headers.accept ]);
    // Activate middleware that checks for correct Mongo _ids when applicable
    activate('request', middleware.configure.checkId);
    // Activate middleware that checks for disabled HTTP methods
    activateOverride('request', middleware.configure.checkMethod);
    // Activate middleware to set request.baucis.conditions for find/remove
    activate('request', 'collection', 'head get del', middleware.configure.conditions);
    // Also activate conditions middleware for update
    activate('request', 'instance', 'put', middleware.configure.conditions);
    // Activate middleware to set request.baucis.count when query is present
    activate('request', 'get', middleware.configure.count);
    // Next, activate the request-stage user middleware.
    activate('request', custom['request']);
    // Activate middleware to build the query (except for POST requests).
    activate('request', middleware.query);

    // __Query-Stage Middleware__
    // The query will have been created (except for POST, which doesn't use a
    // find or remove query).

    // Activate middleware to handle controller and query options.
    activate('query', [ middleware.configure.controller, middleware.configure.query ]);

    // Delete any query instance POST middleware that was added implicitly.
    custom.query.instance.post = [];
    // Delete any query collection PUT middleware that was added implicitly.
    custom.query.collection.put = [];

    // Activate user middleware for the query-stage
    activate('query', custom['query']);

    // Activate middleware to execute the query:

    // Get the count for HEAD requests.
    activate('query', 'head', middleware.exec.count);
    // Execute the find or remove query for GET and DELETE.
    activate('query', 'get del', middleware.exec.exec);
    // Create the documents for a POST request.
    activate('query', 'collection', 'post', middleware.exec.create);
    // Update the documents specified for a PUT request.
    activate('query', 'instance', 'put', middleware.exec.update);

    // Activate some middleware that will set the Link header when that feature
    // is enabled.  (This must come after exec or else the count is
    // returned for all subsequqent executions of the query.)
    if (controller.get('relations') === true) {
      activate('query', 'instance', 'head get post put del', middleware.headers.link);
      activate('query', 'collection', 'head get post put del', middleware.headers.linkCollection);
    }

    // __Document-Stage Middleware__

    // Activate the middleware that sets the `Last-Modified` header when appropriate.
    activate('documents', middleware.documents.lastModified);
    // Activate the the document-stage user middleware.
    activate('documents', custom['documents']);
    // Activate the middleware that sends the resulting document(s) or count.
    activate('documents', middleware.documents.send);

    delete custom;
    initialized = true;
    return controller;
  };
};
<|endoftext|>


<commit_before>const youtubeDecoder = (function(){

	const ytDomain = '.youtube.com';

	const mainDescSelector = '#description.ytd-video-secondary-info-renderer';
	const commentSectionSelector = 'ytd-comments#comments';
	const commentSelector = 'ytd-comment-renderer, ytd-comment-thread-renderer';

	const resultsSelector = 'ytd-search #contents.ytd-item-section-renderer';
	const resultsItemSelector = 'ytd-video-renderer';
	const descSelector = 'yt-formatted-string#description-text';

	const emojiClassNames = emojiReplacer.classNames;

	let pageManagerFound = false;
	let watchingComments = false;

	const debouncedTranslatePage = debounce(translatePage, 200);

	return {
		watchPageChanges,
		isYoutube
	}

	function isYoutube() {
		return document.domain.endsWith(ytDomain);
	}

	function runTranslation(root) {
		domManipulator.start(root);
	}

	function clean() {
		// Due to Youtube’s dynamic data binding,
		// new values get added alongside modified textContent.
		// Delete container nodes to prevent old text from persisting.

		// remove main video description
		const vidDescNode = document.querySelector(
			`${mainDescSelector} > ${descSelector}`
		);
		if (vidDescNode) {
			vidDescNode.parentNode.removeChild(vidDescNode);
		}

		// clear all comments on video pages
		const commentsParent = document.querySelector(
			`${commentSectionSelector} #contents`
		);
		if (commentsParent) {
			while (commentsParent.firstChild) {
				commentsParent.removeChild(commentsParent.firstChild);
			}
		}

		// clear search results
		const resultNodes = document.querySelectorAll(
			`${resultsSelector} > ${resultsItemSelector}`
		);
		for (const node of resultNodes) {
			node.parentNode.removeChild(node);
		}

	}

	function watchPageChanges() {
		if (isYoutube()) {
			window.addEventListener("yt-navigate-start", clean);

			document.body.addEventListener(
				"yt-page-data-updated", debouncedTranslatePage
			);
			document.body.addEventListener(
				"yt-navigate-finish", debouncedTranslatePage
			);

			debouncedTranslatePage();
		}
	}

	function translatePage() {
		if (document.querySelector('ytd-watch-flexy:not([hidden])')) {
			translateVideo();
		} else if (document.querySelector('ytd-search:not([hidden])')) {
			translateSearch();
		}
	}

	function translateVideo() {
		runTranslation(document.querySelector(mainDescSelector));

		const commentsNode = document.querySelector(commentSectionSelector);
		runTranslation(commentsNode);
		watchComments(commentsNode);
	}

	function translateSearch() {
		const descriptionNodes = document.querySelectorAll(descSelector);
		for (const node of descriptionNodes) {
			runTranslation(node);
		}
		waitForSearch();
	}

	function watchComments(commentsNode) {
		if (watchingComments) return;
		new MutationObserver((mutationsList, observer) => {
			for (const mutation of mutationsList) {
				if (mutation.addedNodes.length > 0) {
					for (const node of mutation.addedNodes) {
						if (node.matches && node.matches(commentSelector)) {
							runTranslation(node);
						}
					}
				}
			}
		}).observe(
			commentsNode,
			{ childList: true, subtree:true }
		);
		watchingComments = true;
	}

	function watchSearchResults(searchNode) {
		new MutationObserver((mutationsList, observer) => {
			for (const mutation of mutationsList) {
				if (mutation.addedNodes.length > 0) {
					for (const node of mutation.addedNodes) {
						if (node.matches(resultsItemSelector)) {
							runTranslation(
								node.querySelector(descSelector)
							);
						}
					}
				}
			}
		}).observe(
			searchNode.querySelector(resultsSelector),
			{ childList: true }
		);
	}

	function waitForSearch() {
		if (pageManagerFound) return;
		const pageManagerNode = document.getElementById('page-manager');
		if (pageManagerNode) {
			pageManagerFound = true;
			const searchNode = pageManagerNode.querySelector('ytd-search');
			if (searchNode) {
				watchSearchResults(searchNode);
			} else {
				// wait for search results to be added
				new MutationObserver((mutationsList, observer) => {
					for (const mutation of mutationsList) {
						if (mutation.addedNodes.length > 0) {
							for (const node of mutation.addedNodes) {
								if (node.matches('ytd-search')) {
									observer.disconnect();
									watchSearchResults(node);
								}
							}
						}
					}
				}).observe(
					pageManagerNode,
					{ childList: true }
				);
			}
		}
	}

	// https://davidwalsh.name/javascript-debounce-function
	// Returns a function, that, as long as it continues to be invoked,
	// will not be triggered. The function will be called after it stops
	// being called for [wait] milliseconds. If immediate is passed,
	// trigger the function on the leading edge, instead of the trailing.
	function debounce(func, wait, immediate) {
		var timeout;
		return function() {
			var context = this, args = arguments;
			var later = function() {
				timeout = null;
				if (!immediate) func.apply(context, args);
			};
			var callNow = immediate && !timeout;
			clearTimeout(timeout);
			timeout = setTimeout(later, wait);
			if (callNow) func.apply(context, args);
		};
	};

}());
<commit_msg>added support for youtube comments/descriptions on mobile<commit_after>const youtubeDecoder = (function(){

	const ytDomain = '.youtube.com';

	const mainDescSelector = '#description.ytd-video-secondary-info-renderer';
	const commentSectionSelector =
		'ytd-comments#comments, ytm-comment-section-renderer';
	const commentSelector = 'ytd-comment-renderer, ytm-comment-renderer';

	const resultsSelector = 'ytd-search #contents.ytd-item-section-renderer';
	const resultsItemSelector = 'ytd-video-renderer';
	const descSelector = 'yt-formatted-string#description-text';

	const mobileDescParentSelector = 'ytm-slim-video-metadata-renderer';

	const emojiClassNames = emojiReplacer.classNames;

	let pageManagerFound = false;
	let watchedCommentsNode;

	const debouncedTranslatePage = debounce(translatePage, 200);

	return {
		watchPageChanges,
		isYoutube
	}

	function isYoutube() {
		return document.domain.endsWith(ytDomain);
	}

	function runTranslation(root) {
		domManipulator.start(root);
	}

	function clean() {
		// Due to Youtube’s dynamic data binding,
		// new values get added alongside modified textContent.
		// Delete container nodes to prevent old text from persisting.

		// remove main video description
		const vidDescNode = document.querySelector(
			`${mainDescSelector} > ${descSelector}`
		);
		if (vidDescNode) {
			vidDescNode.parentNode.removeChild(vidDescNode);
		}

		// clear all comments on video pages
		const commentsRoot = document.querySelector(commentSectionSelector);
		if (commentsRoot) {
			const commentsParent = commentsRoot.querySelector('#contents');
			if (commentsParent) {
				while (commentsParent.firstChild) {
					commentsParent.removeChild(commentsParent.firstChild);
				}
			}
		}

		// clear search results
		const resultNodes = document.querySelectorAll(
			`${resultsSelector} > ${resultsItemSelector}`
		);
		for (const node of resultNodes) {
			node.parentNode.removeChild(node);
		}

	}

	function watchPageChanges() {
		if (isYoutube()) {
			debouncedTranslatePage();

			window.addEventListener("yt-navigate-start", clean);

			document.body.addEventListener(
				"yt-page-data-updated", debouncedTranslatePage
			);
			document.body.addEventListener(
				"yt-navigate-finish", debouncedTranslatePage
			);

			// mobile
			document.documentElement.addEventListener(
				"video-data-change", debouncedTranslatePage
			);
		}
	}

	function translatePage() {
		if (document.querySelector(
			'ytd-watch-flexy:not([hidden]), ytm-watch'
		)) {
			translateVideo();
		} else if (document.querySelector('ytd-search:not([hidden])')) {
			translateSearch();
		}
	}

	function translateVideo() {
		runTranslation(document.querySelector(mainDescSelector));

		const commentsNode = document.querySelector(commentSectionSelector);
		runTranslation(commentsNode);
		watchComments(commentsNode);

		// on mobile, descriptions dynamically loaded after opening accordion
		watchDescMobile();
	}

	function translateSearch() {
		const descriptionNodes = document.querySelectorAll(descSelector);
		for (const node of descriptionNodes) {
			runTranslation(node);
		}
		waitForSearch();
	}

	function watchComments(commentsNode) {
		if (watchedCommentsNode !== commentsNode) {
			new MutationObserver((mutationsList, observer) => {
				for (const mutation of mutationsList) {
					if (mutation.addedNodes.length > 0) {
						for (const node of mutation.addedNodes) {
							if (node.matches && node.matches(commentSelector)) {
								runTranslation(node);
							}
						}
					}
				}
			}).observe(
				commentsNode,
				{ childList: true, subtree:true }
			);
			watchedCommentsNode = commentsNode;
		}
	}

	function watchSearchResults(searchNode) {
		new MutationObserver((mutationsList, observer) => {
			for (const mutation of mutationsList) {
				if (mutation.addedNodes.length > 0) {
					for (const node of mutation.addedNodes) {
						if (node.matches(resultsItemSelector)) {
							runTranslation(
								node.querySelector(descSelector)
							);
						}
					}
				}
			}
		}).observe(
			searchNode.querySelector(resultsSelector),
			{ childList: true }
		);
	}

	function waitForSearch() {
		if (pageManagerFound) return;
		const pageManagerNode = document.getElementById('page-manager');
		if (pageManagerNode) {
			pageManagerFound = true;
			const searchNode = pageManagerNode.querySelector('ytd-search');
			if (searchNode) {
				watchSearchResults(searchNode);
			} else {
				// wait for search results to be added
				new MutationObserver((mutationsList, observer) => {
					for (const mutation of mutationsList) {
						if (mutation.addedNodes.length > 0) {
							for (const node of mutation.addedNodes) {
								if (node.matches('ytd-search')) {
									observer.disconnect();
									watchSearchResults(node);
								}
							}
						}
					}
				}).observe(
					pageManagerNode,
					{ childList: true }
				);
			}
		}
	}

	function watchDescMobile() {
		const descContainer = document.querySelector(mobileDescParentSelector);
		if (descContainer) {
			new MutationObserver((mutationsList, observer) => {
				for (const mutation of mutationsList) {
					if (mutation.addedNodes.length > 0) {
						for (const node of mutation.addedNodes) {
							if (node.matches('.user-text')) {
								runTranslation(node);
							}
						}
					}
				}
			}).observe(
				descContainer,
				{ childList: true }
			);
		}
	}

	// https://davidwalsh.name/javascript-debounce-function
	// Returns a function, that, as long as it continues to be invoked,
	// will not be triggered. The function will be called after it stops
	// being called for [wait] milliseconds. If immediate is passed,
	// trigger the function on the leading edge, instead of the trailing.
	function debounce(func, wait, immediate) {
		var timeout;
		return function() {
			var context = this, args = arguments;
			var later = function() {
				timeout = null;
				if (!immediate) func.apply(context, args);
			};
			var callNow = immediate && !timeout;
			clearTimeout(timeout);
			timeout = setTimeout(later, wait);
			if (callNow) func.apply(context, args);
		};
	};

}());
<|endoftext|>


<commit_before>(function () {
	var global = this;
	WordsAlive = function () {};
	WordsAlive.addons = {};
	WordsAlive.prototype = {
		getNewNodes : function (root) {
			var walker = document.createTreeWalker(
					root||document.body,
					NodeFilter.SHOW_TEXT,
					null,
					false);
			var node;
			var nodes = [];
			while (node = walker.nextNode()) {
				var val = node.nodeValue;
				if (!val || !val.replace(/\s+/g, ''))
					continue;
				var parent = $(node.parentNode);
				if (!parent.is(':visible'))
					continue;
				if (parent.is('.wordsAlive'))
					continue;
				nodes.push(node);
			}
			return nodes;
		},
		getManagers : function (word) {
			if (!word)
				return;
			var managers = [];
			Object.keys(WordsAlive.addons).forEach(function (key) {
				var addon = WordsAlive.addons[key];
				var manager = addon.getManager(word);
				if (manager)
					managers.push(manager);
			});
			return managers;
		},
		run : function (root) {
			var self = this;
			var nodes = this.getNewNodes(root);
			var newElems = [];
			nodes.forEach(function (node) {
				var val = node.nodeValue;
				var splits = val.split(/[\r\n]+/g);
				var changed = false;
				for (var i = 0; i < splits.length; i++) {
					splits[i] = splits[i].replace(/[a-z]+/ig, function (m) {
							var word = m.toLowerCase();
							var wordManagers = self.getManagers(word);
							if (!wordManagers || !wordManagers.length)
								return m;
							changed = true;
							var classes = {};
							for (var j = 0; j < wordManagers.length; j++) {
								var arr = wordManagers[j].getClasses(word) || [];
								arr.forEach(function (cls) {
									classes[cls] = true;
								});
							}
							return '' + m + '';
						});
				}
				if (!changed)
					return;
				var newElem = document.createElement('span');
				newElem.className = 'wordsAlive';
				newElem.innerHTML = splits.join('\r\n');
				node.parentNode.replaceChild(newElem, node);
				newElems.push(newElem);
			});
			this.initializeAddons(newElems);
		},
		initializeAddons(newElems) {
			Object.keys(WordsAlive.addons).forEach(function (key) {
				var addon = WordsAlive.addons[key];
				addon.init(newElems);
			});
		}
	};
	global.WordsAlive = WordsAlive;
})();<commit_msg>Added files via upload<commit_after>(function () {
	var global = this;
	WordsAlive = function () {};
	WordsAlive.addons = {};
	WordsAlive.prototype = {
		getNewNodes : function (root) {
			var walker = document.createTreeWalker(
					root||document.body,
					NodeFilter.SHOW_TEXT,
					null,
					false);
			var node;
			var nodes = [];
			while (node = walker.nextNode()) {
				var val = node.nodeValue;
				if (!val || !val.replace(/\s+/g, ''))
					continue;
				var parent = $(node.parentNode);
				if (!parent.is(':visible'))
					continue;
				if (parent.is('.wordsAlive')||parent.closest('.wordsAlive').length)
					continue;
				nodes.push(node);
			}
			return nodes;
		},
		getManagers : function (word) {
			if (!word)
				return;
			var managers = [];
			Object.keys(WordsAlive.addons).forEach(function (key) {
				var addon = WordsAlive.addons[key];
				var manager = addon.getManager(word);
				if (manager)
					managers.push(manager);
			});
			return managers;
		},
		run : function (root) {
			var self = this;
			var nodes = this.getNewNodes(root);
			var newElems = [];
			nodes.forEach(function (node) {
				var val = node.nodeValue;
				var splits = val.split(/[\r\n]+/g);
				var changed = false;
				for (var i = 0; i < splits.length; i++) {
					splits[i] = splits[i].replace(/[a-z]+/ig, function (m) {
							var word = m.toLowerCase();
							var wordManagers = self.getManagers(word);
							if (!wordManagers || !wordManagers.length)
								return m;
							changed = true;
							var classes = {};
							for (var j = 0; j < wordManagers.length; j++) {
								var arr = wordManagers[j].getClasses(word) || [];
								arr.forEach(function (cls) {
									classes[cls] = true;
								});
							}
							return '' + m + '';
						});
				}
				if (!changed)
					return;
				var newElem = document.createElement('span');
				newElem.className = 'wordsAlive';
				newElem.innerHTML = splits.join('\r\n');
				node.parentNode.replaceChild(newElem, node);
				newElems.push(newElem);
			});
			this.initializeAddons(newElems);
		},
		initializeAddons(newElems) {
			Object.keys(WordsAlive.addons).forEach(function (key) {
				var addon = WordsAlive.addons[key];
				addon.init(newElems);
			});
		}
	};
	global.WordsAlive = WordsAlive;
})();<|endoftext|>


<commit_before>/*
 * NoR
 * http://vic.github.com/NoR
 *
 * Copyright (c) 2012 Victor Borja
 * Licensed under the MIT license.
 */

(function(exports) {

  exports.awesome = function() {
    return 'awesome';
  };

}(typeof exports === 'object' && exports || this));
<commit_msg>Added NoR<commit_after>/*
 * NoR - There's No Return. A javascript reactive programming engine.
 * http://vic.github.com/NoR
 *
 * Copyright (c) 2012 Victor Borja
 * Licensed under the MIT license.
 */

(function(exports) {

  /* A cell holds a value and notify observers when it has changed */
  var cell = function(value, name) {

    var cell = function() {
      if(arguments.length === 0) {
        return value
      } else if(value !== arguments[0]) {
        notifySubscribers(value = arguments[0])
      }
    }

    var subscribers = []

    var notifySubscribers = function(value) {
      for(var i in subscribers) {
        if(subscribers[i]) { subscribers[i](value) }
      }
    }

    var addSubscriber = function(subscriber) {
      var i = subscribers.indexOf(subscriber)
      if(i < 0 ) { subscribers.push(subscriber) }
    }

    var removeSubscriber = function(subscriber) {
      var i = subscribers.indexOf(subscriber)
      if(i > -1) { subscribers[i] = null }
      // TODO: clean list when too big
    }

    cell.toString = function() { return '{'+name+':'+value+'}' }
    cell.subscribe = addSubscriber
    cell.bind = function(other) {
      cell.subscribe(other)
      other.subscribe(cell)
    }

    return cell
  }

  var gate = function(impl, self, undefined){
    var args_re = /^function\s+\(([^\)]+)\)/, sep_re = /,?\s+/
      , names = impl.toString().match(args_re)[1].split(sep_re)
      , self = self || new cell
      , cells = []
      , timeout
      , execute = function() {
        if(timeout) { clearTimeout(timeout) }
        timeout = setTimeout(function(){
          impl.apply(self(), cells)
        }, 0)
      }
      , invoke = function() {
        if(this instanceof arguments.callee){
          var g = gate(impl, self)
          for(var i in arguments){
            if(arguments[i] instanceof cell){
              arguments[i].bind(g[i])
              g[i](arguments[i]())

            } else {
              g[i](arguments[i])
            }
          }
          return g
        }
        for(var i in arguments) {
          cells[i](arguments[i])
        }
      }
    self.subscribe(execute)
    for(var i in names) {
      invoke[names[i]] = invoke[i] = cells[i] = cell(undefined, names[i])
      cells[i].subscribe(execute)
    }
    invoke.toString = function(){
      var buff = []
      for(var i in names) {
        buff.push(names[i]+':'+cells[i]())
      }
      return '{'+buff.join(', ')+'}'
    }
    return invoke
  }

  exports.NoR = function(impl, self) {
    return gate(impl, self)
  }

  exports.awesome = function() {
    return 'awesome';
  };

}(typeof exports === 'object' && exports || this));
<|endoftext|>


<commit_before>import {placeholderChar as defaultPlaceholderChar} from './constants.js'

const emptyArray = []

export function convertMaskToPlaceholder(mask = emptyArray, placeholderChar = defaultPlaceholderChar) {
  if (mask.indexOf(placeholderChar) !== -1) {
    throw new Error(
      'Placeholder character must not be used as part of the mask. Please specify a character ' +
      'that is not present in your mask as your placeholder character.\n\n' +
      `The placeholder character that was received is: ${JSON.stringify(placeholderChar)}\n\n` +
      `The mask that was received is: ${JSON.stringify(mask)}`
    )
  }

  return mask.map((char) => {
    return (char instanceof RegExp) ? placeholderChar : char
  }).join('')
}

export function isString(value) {
  return typeof value === 'string' || value instanceof String
}

export function isNumber(value) {
  return value && value.length === undefined && !isNaN(value)
}

const strCaretTrap = '[]'
export function processCaretTraps(mask) {
  const indexes = []

  let indexOfCaretTrap
  while(indexOfCaretTrap = mask.indexOf(strCaretTrap), indexOfCaretTrap !== -1) { // eslint-disable-line
    indexes.push(indexOfCaretTrap)

    mask.splice(indexOfCaretTrap, 1)
  }

  return {maskWithoutCaretTraps: mask, indexes}
}
<commit_msg>Fix an issue with `isNumber` utility function (#204)<commit_after>import {placeholderChar as defaultPlaceholderChar} from './constants.js'

const emptyArray = []

export function convertMaskToPlaceholder(mask = emptyArray, placeholderChar = defaultPlaceholderChar) {
  if (mask.indexOf(placeholderChar) !== -1) {
    throw new Error(
      'Placeholder character must not be used as part of the mask. Please specify a character ' +
      'that is not present in your mask as your placeholder character.\n\n' +
      `The placeholder character that was received is: ${JSON.stringify(placeholderChar)}\n\n` +
      `The mask that was received is: ${JSON.stringify(mask)}`
    )
  }

  return mask.map((char) => {
    return (char instanceof RegExp) ? placeholderChar : char
  }).join('')
}

export function isString(value) {
  return typeof value === 'string' || value instanceof String
}

export function isNumber(value) {
  return typeof value === 'number' && value.length === undefined && !isNaN(value)
}

const strCaretTrap = '[]'
export function processCaretTraps(mask) {
  const indexes = []

  let indexOfCaretTrap
  while(indexOfCaretTrap = mask.indexOf(strCaretTrap), indexOfCaretTrap !== -1) { // eslint-disable-line
    indexes.push(indexOfCaretTrap)

    mask.splice(indexOfCaretTrap, 1)
  }

  return {maskWithoutCaretTraps: mask, indexes}
}
<|endoftext|>


<commit_before>var _ = require('lodash'); // Utilities. http://underscorejs.org/
var Backbone = require('backbone'); // Data model utilities. http://backbonejs.org/
var winston = require('winston'); // Logging. https://github.com/flatiron/winston
var fs = require('node-fs'); // Recursive directory creation. https://github.com/bpedro/node-fs

var BaseModel = require('./baseModel.js').BaseModel;
var Network = require('./network.js').Network;
var ContentUpdater = require('./contentUpdater.js').ContentUpdater;
var AppUpdater = require('./appUpdater.js').AppUpdater;
var Persistence = require('./persistence.js').Persistence;
var AppState = require('./appState.js').AppState;
var Logging = require('./logging.js').Logging;

// Model for app logic specific to the server.
exports.ServerState = BaseModel.extend({
    defaults: {
        contentUpdater: null,
        appUpdater: null,
        persistence: null,
        appState: null,
        logging: null,
        network: null
    },

    start: function() {
        this.set('network', new Network({
            config: config.network
        }));
        this.set('contentUpdater', new ContentUpdater({
            name: 'content',
            config: config.contentUpdater
        }));
        this.set('appUpdater', new AppUpdater({
            name: 'app',
            config: config.appUpdater
        }));
        this.set('persistence', new Persistence({
            config: config.persistence
        }));
        this.set('logging', new Logging({
            config: config.logging
        }));
        this.set('appState', new AppState({
            config: config.app
        }));

        // Spew config for documentation.
        // console.log(JSON.stringify(this.fullConfig(), null, '\t'));

        comm.socketToConsole.sockets.on('connection', _.bind(this._onConnection, this));
    },

    fullConfig: function() {
        return {
            network: this.get('network').attributes,
            contentUpdater: this.get('contentUpdater').attributes,
            appUpdater: this.get('appUpdater').attributes,
            persistence: this.get('persistence').attributes,
            logging: this.get('logging').attributes
        };
    },

    clean: function() {
        this.get('network').clean();
        this.get('contentUpdater').clean();
        this.get('appUpdater').clean();
        this.get('persistence').clean();
        this.get('logging').clean();
        this.get('appState').clean();
        BaseModel.prototype.clean.apply(this);
    },

    _onConnection: function(socket) {
        socket.on('setSource', _.bind(this.setSource, this));
        socket.on('update', _.bind(this.update, this));
        socket.on('rollBack', _.bind(this.rollback, this));
    },

    setSource: function(updater, source) {
        updater = this.get(updater + 'Updater');
        if (source == updater.get('source')) {
            return;
        }

        updater.set('source', source);

        fs.exists(updater.get('temp')[source], _.bind(function(exists) {
            if (exists) {
                this.deploy(updater, true);
            } else {
                this.update(updater);
            }
        }, this));
    },

    _deploySource: function(contentChecked, appChecked, contentDownloaded, appDownloaded, force) {
        if (appChecked && contentChecked) {
            if (contentDownloaded && appDownloaded) {
                this._onDownloaded(contentDownloaded, appDownloaded, force);
            } else {
                this.update();
            }
        }
    },

    update: function(updater) {
        if (_.isString(updater)) {
            updater = this.get(updater + 'Updater');
        }

        logger.info('Updating ' + updater.get('name') + ' from ' + updater.get('source'));

        updater.download(_.bind(function(error) {
            if (error) {
                return;
            }

            logger.info(updater.get('name') + ' download complete! ' + (updater.get('needsUpdate') ? '' : 'Nothing new was found.'));
            if (!updater.get('needsUpdate')) {
                return;
            }

            this.deploy(updater);
        }, this));
    },

    deploy: function(updater, force) {
        this.get('persistence').shutdownApp(_.bind(function() {

            // Copy content files from the temp folder.
            updater.deploy(force, _.bind(function(error) {
                if (!error) {
                    logger.info(updater.get('name') + ' deploy complete!');
                }

                this.get('persistence').restartApp();
            }, this));
        }, this));
    },

    // Shut down the app, roll back content, and restart it.
    rollback: function(updater) {
        updater = this.get(updater + 'Updater');
        this.get('persistence').shutdownApp(_.bind(function() {
            updater.rollBack(_.bind(function() {
                logger.info('Rollback complete!');
                this.get('persistence').restartApp();
            }, this));
        }, this));
    }
});<commit_msg>don't restart app after update unless it was already running, fixes #7316<commit_after>var _ = require('lodash'); // Utilities. http://underscorejs.org/
var Backbone = require('backbone'); // Data model utilities. http://backbonejs.org/
var winston = require('winston'); // Logging. https://github.com/flatiron/winston
var fs = require('node-fs'); // Recursive directory creation. https://github.com/bpedro/node-fs

var BaseModel = require('./baseModel.js').BaseModel;
var Network = require('./network.js').Network;
var ContentUpdater = require('./contentUpdater.js').ContentUpdater;
var AppUpdater = require('./appUpdater.js').AppUpdater;
var Persistence = require('./persistence.js').Persistence;
var AppState = require('./appState.js').AppState;
var Logging = require('./logging.js').Logging;

// Model for app logic specific to the server.
exports.ServerState = BaseModel.extend({
    defaults: {
        contentUpdater: null,
        appUpdater: null,
        persistence: null,
        appState: null,
        logging: null,
        network: null
    },

    start: function() {
        this.set('network', new Network({
            config: config.network
        }));
        this.set('contentUpdater', new ContentUpdater({
            name: 'content',
            config: config.contentUpdater
        }));
        this.set('appUpdater', new AppUpdater({
            name: 'app',
            config: config.appUpdater
        }));
        this.set('persistence', new Persistence({
            config: config.persistence
        }));
        this.set('logging', new Logging({
            config: config.logging
        }));
        this.set('appState', new AppState({
            config: config.app
        }));

        // Spew config for documentation.
        // console.log(JSON.stringify(this.fullConfig(), null, '\t'));

        comm.socketToConsole.sockets.on('connection', _.bind(this._onConnection, this));
    },

    fullConfig: function() {
        return {
            network: this.get('network').attributes,
            contentUpdater: this.get('contentUpdater').attributes,
            appUpdater: this.get('appUpdater').attributes,
            persistence: this.get('persistence').attributes,
            logging: this.get('logging').attributes
        };
    },

    clean: function() {
        this.get('network').clean();
        this.get('contentUpdater').clean();
        this.get('appUpdater').clean();
        this.get('persistence').clean();
        this.get('logging').clean();
        this.get('appState').clean();
        BaseModel.prototype.clean.apply(this);
    },

    _onConnection: function(socket) {
        socket.on('setSource', _.bind(this.setSource, this));
        socket.on('update', _.bind(this.update, this));
        socket.on('rollBack', _.bind(this.rollback, this));
    },

    setSource: function(updater, source) {
        if (_.isString(updater)) {
            updater = this.get(updater + 'Updater');
        }

        if (source == updater.get('source')) {
            return;
        }

        updater.set('source', source);

        fs.exists(updater.get('temp')[source], _.bind(function(exists) {
            if (exists) {
                this.deploy(updater, true);
            } else {
                this.update(updater);
            }
        }, this));
    },

    _deploySource: function(contentChecked, appChecked, contentDownloaded, appDownloaded, force) {
        if (appChecked && contentChecked) {
            if (contentDownloaded && appDownloaded) {
                this._onDownloaded(contentDownloaded, appDownloaded, force);
            } else {
                this.update();
            }
        }
    },

    update: function(updater) {
        if (_.isString(updater)) {
            updater = this.get(updater + 'Updater');
        }

        logger.info('Updating ' + updater.get('name') + ' from ' + updater.get('source'));

        updater.download(_.bind(function(error) {
            if (error) {
                return;
            }

            logger.info(updater.get('name') + ' download complete! ' + (updater.get('needsUpdate') ? '' : 'Nothing new was found.'));
            if (!updater.get('needsUpdate')) {
                return;
            }

            this.deploy(updater);
        }, this));
    },

    deploy: function(updater, force) {
        if (_.isString(updater)) {
            updater = this.get(updater + 'Updater');
        }

        doDeploy = function(callback) {
            // Copy content files from the temp folder.
            updater.deploy(force, _.bind(function(error) {
                if (!error) {
                    logger.info(updater.get('name') + ' deploy complete!');
                }

                if (callback) {
                    callback();
                }
            }, this));
        };

        // Just deploy.
        if (!this.get('appState').get('isRunning')) {
            doDeploy();
            return;
        }

        // Shutdown, deploy, restart.
        this.get('persistence').shutdownApp(_.bind(function() {
            doDeploy(_.bind(function() {
                this.get('persistence').restartApp();
            }, this));
        }, this));
    },

    // Shut down the app, roll back content, and restart it.
    rollback: function(updater) {
        if (_.isString(updater)) {
            updater = this.get(updater + 'Updater');
        }

        doRollback = function(callback) {
            updater.rollBack(_.bind(function() {
                logger.info('Rollback complete!');
                if (callback) {
                    callback();
                }
            }, this));
        };

        // Just rollback.
        if (!this.get('appState').get('isRunning')) {
            doRollback();
            return;
        }

        // Shutdown, rollback, restart.
        this.get('persistence').shutdownApp(_.bind(function() {
            doRollback(_.bind(function() {
                this.get('persistence').restartApp();
            }, this));
        }, this));
    }
});<|endoftext|>


<commit_before>;(function(global, factory) {
  // Use UMD pattern to expose exported functions
  if (typeof exports === 'object') {
    // Expose to Node.js
    module.exports = factory();
  } else if (typeof define === 'function' && define.amd) {
    // Expose to RequireJS
    define([], factory);
  }

  // Expose to global object (likely browser window)
  var exports = factory();
  for (var prop in exports) {
    global[prop] = exports[prop];
  }
}(this, function() {
  var numIntervals = 0,
    intervals = {};

  var setCorrectingInterval = function(func, delay) {
    var id = numIntervals++,
      planned = Date.now() + delay;

    // Normalize func as function
    switch (typeof func) {
      case 'function':
        break;
      case 'string':
        var sFunc = func;
        func = function() {
          eval(sFunc);
        };
        break;
      default:
        func = function() { };
    }

    function tick() {
      func();

      if (intervals[id]) {
        planned += delay;
        intervals[id] = setTimeout(tick, planned - Date.now());
      }
    }

    intervals[id] = setTimeout(tick, delay);
    return id;
  };

  var clearCorrectingInterval = function(id) {
    clearTimeout(intervals[id]);
    delete intervals[id];
  };

  return {
    setCorrectingInterval: setCorrectingInterval,
    clearCorrectingInterval: clearCorrectingInterval
  };
}));<commit_msg>Re-implement Date.now polyfill<commit_after>;(function(global, factory) {
  // Use UMD pattern to expose exported functions
  if (typeof exports === 'object') {
    // Expose to Node.js
    module.exports = factory();
  } else if (typeof define === 'function' && define.amd) {
    // Expose to RequireJS
    define([], factory);
  }

  // Expose to global object (likely browser window)
  var exports = factory();
  for (var prop in exports) {
    global[prop] = exports[prop];
  }
}(this, function() {
  var numIntervals = 0,
    intervals = {};

  // Polyfill Date.now
  var now = Date.now || function() {
    return new Date().valueOf();
  };

  var setCorrectingInterval = function(func, delay) {
    var id = numIntervals++,
      planned = now() + delay;

    // Normalize func as function
    switch (typeof func) {
      case 'function':
        break;
      case 'string':
        var sFunc = func;
        func = function() {
          eval(sFunc);
        };
        break;
      default:
        func = function() { };
    }

    function tick() {
      func();

      if (intervals[id]) {
        planned += delay;
        intervals[id] = setTimeout(tick, planned - now());
      }
    }

    intervals[id] = setTimeout(tick, delay);
    return id;
  };

  var clearCorrectingInterval = function(id) {
    clearTimeout(intervals[id]);
    delete intervals[id];
  };

  return {
    setCorrectingInterval: setCorrectingInterval,
    clearCorrectingInterval: clearCorrectingInterval
  };
}));<|endoftext|>


<commit_before>Milestones = new Mongo.Collection('milestones');

Milestones.validTypes = ['strategic', 'year', 'month', 'week'];

Milestones.attachSchema(new SimpleSchema({
  'period': { 'type': String, 'optional': true },
  'type': { 'type': String, 'allowedValues': Milestones.validTypes },
  'userId': { 'type': String },
  'parentId': { 'type': String, 'optional': true },
  'startsAt': { 'type': Date, 'optional': true, 'label': 'Start Period' },
  'endsAt': { 'type': Date, 'optional': true, 'label': 'End Period' }
}));

Milestones.boundsFor = (date, type) => {
  if (type === 'strategic') {
    return {
      'startsAt': moment(2000, 'YYYY').toDate(),
      'endsAt': moment().add(100, 'y').toDate()
    };
  }
  return {
    'startsAt': Milestones.bound(date.clone().startOf(type), 'start').toDate(),
    'endsAt': Milestones.bound(date.clone().endOf(type), 'end').toDate()
  };
};

// Returns nearest week bound for given date
Milestones.bound = (date, type = 'start') => {
  let _date = date;
  if (type === 'start' && date.day() > 4) {
    _date = date.add(1, 'w');
  }
  if (type === 'end' && date.day() <= 4) {
    _date = date.subtract(1, 'w');
  }
  return _date[`${type}Of`]('isoWeek');
};

Milestones.relativeType = (type, levelDiff) => {
  return Milestones.validTypes[Milestones.validTypes.indexOf(type) + levelDiff];
};

Milestones.periodFormats = extended => ({
  'year': { 'parse': 'YYYY', 'display': 'YYYY' },
  'month': { 'parse': 'YYYY-MM', 'display': extended ? 'MMMM YYYY' : 'MMM' },
  'week': { 'parse': '', 'display': extended ? 'DD MMMM YYYY' : 'DD MMM' }
});

Milestones.helpers({
  'goals': function() {
    return Goals.find({'milestoneId': this._id}, {'sort': {'priority': 1}});
  },
  'parent': function() { return Milestones.findOne(this.parentId); },
  'children': function() {
    return Milestones.find({'parentId': this._id}, {'sort': {'startsAt': -1}});
  },
  'title': function(extended) {
    let format = Milestones.periodFormats(extended)[this.type];
    if (this.type === 'strategic') return 'Strategic';
    if (this.type === 'week') {
      let periods = [
        moment(this.period).startOf('week').format('DD'),
        moment(this.period).endOf('week').format(format.display)
      ];
      return periods.join('-');
    }

    return moment(this.period, format.parse).format(format.display);
  },
  'progress': function() {
    let sum = this.goals()
      .map(goal => goal.completedPct)
      .filter(Number)
      .reduce((a, b) => a + b, 0);
    return Math.round(sum / this.goals().count());
  },
  'isCurrent': function() {
    return (this.type !== 'strategic') &&
      moment(this.startsAt).isSameOrBefore() &&
      moment(this.endsAt).isSameOrAfter();
  }
});

Milestones.allow({
  'insert': function(userId, doc) {
    let milestone = Milestones.findOne({
      'userId': userId,
      'period': doc.period,
      'type': doc.type
    });
    if (!!milestone) {
      throw new Meteor.Error('period-invalid',
        'Milestone for this period already created!');
    }
    return userId === doc.userId && !milestone;
  },
  'update': function(userId, doc) { return userId === doc.userId; },
  'remove': function() { return false; }
});
<commit_msg>Milestones#parent and #child rely on time bounds<commit_after>Milestones = new Mongo.Collection('milestones');

Milestones.validTypes = ['strategic', 'year', 'month', 'week'];

Milestones.attachSchema(new SimpleSchema({
  'period': { 'type': String, 'optional': true },
  'type': { 'type': String, 'allowedValues': Milestones.validTypes },
  'userId': { 'type': String },
  'parentId': { 'type': String, 'optional': true },
  'startsAt': { 'type': Date, 'optional': true, 'label': 'Start Period' },
  'endsAt': { 'type': Date, 'optional': true, 'label': 'End Period' }
}));

Milestones.boundsFor = (date, type) => {
  if (type === 'strategic') {
    return {
      'startsAt': moment(2000, 'YYYY').toDate(),
      'endsAt': moment().add(100, 'y').toDate()
    };
  }
  return {
    'startsAt': Milestones.bound(date.clone().startOf(type), 'start').toDate(),
    'endsAt': Milestones.bound(date.clone().endOf(type), 'end').toDate()
  };
};

// Returns nearest week bound for given date
Milestones.bound = (date, type = 'start') => {
  let _date = date;
  if (type === 'start' && date.day() > 4) {
    _date = date.add(1, 'w');
  }
  if (type === 'end' && date.day() <= 4) {
    _date = date.subtract(1, 'w');
  }
  return _date[`${type}Of`]('isoWeek');
};

Milestones.relativeType = (type, levelDiff) => {
  return Milestones.validTypes[Milestones.validTypes.indexOf(type) + levelDiff];
};

Milestones.periodFormats = extended => ({
  'year': { 'parse': 'YYYY', 'display': 'YYYY' },
  'month': { 'parse': 'YYYY-MM', 'display': extended ? 'MMMM YYYY' : 'MMM' },
  'week': { 'parse': '', 'display': extended ? 'DD MMMM YYYY' : 'DD MMM' }
});

Milestones.helpers({
  'goals': function() {
    return Goals.find({'milestoneId': this._id}, {'sort': {'priority': 1}});
  },
  'parent': function() {
    return Milestones.findOne({
      'type': Milestones.relativeType(this.type, -1),
      'startsAt': { '$lte': this.startsAt },
      'endsAt': { '$gte': this.endsAt }
    });
  },
  'children': function() {
    return Milestones.find({
      'type': Milestones.relativeType(this.type, +1),
      'startsAt': { '$gte': this.startsAt },
      'endsAt': { '$lte': this.endsAt }
    }, {'sort': {'startsAt': -1}});
  },
  'title': function(extended) {
    let format = Milestones.periodFormats(extended)[this.type];
    if (this.type === 'strategic') return 'Strategic';
    if (this.type === 'week') {
      let periods = [
        moment(this.period).startOf('week').format('DD'),
        moment(this.period).endOf('week').format(format.display)
      ];
      return periods.join('-');
    }

    return moment(this.period, format.parse).format(format.display);
  },
  'progress': function() {
    let sum = this.goals()
      .map(goal => goal.completedPct)
      .filter(Number)
      .reduce((a, b) => a + b, 0);
    return Math.round(sum / this.goals().count());
  },
  'isCurrent': function() {
    return this.type !== 'strategic' &&
      moment(this.startsAt).isSameOrBefore() &&
      moment(this.endsAt).isSameOrAfter();
  }
});

Milestones.allow({
  'insert': function(userId, doc) {
    let milestone = Milestones.findOne({
      'userId': userId,
      'period': doc.period,
      'type': doc.type
    });
    if (!!milestone) {
      throw new Meteor.Error('period-invalid',
        'Milestone for this period already created!');
    }
    return userId === doc.userId && !milestone;
  },
  'update': function(userId, doc) { return userId === doc.userId; },
  'remove': function() { return false; }
});
<|endoftext|>


<commit_before>'use strict';

ArZeit.service('TimerModel',
    function(){
        var _timers = [
                {id: 0, title: 'test timer 1', active: false},
                {id: 1, title: 'test timer 2', active: false},
                {id: 2, title: 'test timer 3', active: false},
            ];
        this.getTimers = function(){
            return _timers;
        };
        this.getTimer = function(id){
            return _timers[id];
        };
    }
);
<commit_msg>adding simple GET call to arzeit_server that does not work because of cross origin policy<commit_after>'use strict';

ArZeit.service('TimerModel',
    function($http){
        var _timers = [
            {id: 0, title: 'test timer 1', active: false},
            {id: 1, title: 'test timer 2', active: false},
            {id: 2, title: 'test timer 3', active: false},
        ];

        var timer_pk_regex = /timers\/(\d+)\//g;

        function loadTimers(data, status, headers, config){
            for (var i; i < data.length; i++){
                var timer_data = data[i];
                var timer_id = timer_pk_regex.exec(data.url)[1];
                var timer = {
                    id: timer_id,
                    title: timer_data.name,
                    active: timer_data.active,
                };
                _timers.push(timer);
            }
        }

        this.getTimers = function(){
            $http.get('http://localhost:8000/timers/').success(loadTimers);
            // cross origin policy violated even for different ports...
            return _timers;
        };

        this.getTimer = function(id){
            return _timers[id];
        };

    }
);
<|endoftext|>


<commit_before>var mongoose     = require('mongoose');
var Schema       = mongoose.Schema;

var ObjectId = mongoose.Schema.Types.ObjectId;
var Location = require('./location_model');
var Space = require('./space_model');
var User = require('./user_model');
var rest = require("restler-q");
var config = require("config");

var LeadSchema   = new Schema({
	name: { type: String, index: true },
	organisation: String,
	location_id: { type: ObjectId, index: true, ref: "Location" },
	space_id: { type: ObjectId, index: true, ref: "Space" },
	email: { type: String, index: true },
	mobile: String,
	date_created: { type: Date, default: Date.now },
	source: String,
	url: String,
	type: String,
	intercom_id: String,
	mailtemplate_id: ObjectId,
	membership_id: { type: ObjectId, ref: "Membership" },
	short_name: String,
	legal_name: String,
	accounts_email: String,
	website: String,
	address: String,
	postal_address: String,
	vat: String,
	company_registration_number: String,
	seats: Number,
	spam: { type: Boolean, default: false },
	data: mongoose.Schema.Types.Mixed,
	"g-recaptcha-response": String,
	referral_user_id: { type: ObjectId, ref: "User" },
	referral_date_paid: Date,
	referral_amount: Number,
	_deleted: { type: Boolean, default: false, index: true },
	_owner_id: ObjectId
});

LeadSchema.set("_perms", {
	admin: "cru",
	super_user: "crud",
	user: "cr",
	all: "c",
});

LeadSchema.pre("save", function(next) {
	var Lead = require("./lead_model");
	var transaction = this;
	Lead.findOne({ _id: transaction._id })
	.then(result => {
		if (result) // Is an edit
			return next();
		if (this.__user) {
			this.spam = false;
			return next();
		}
		if (this["g-recaptcha-response"]) {
			rest.post(config.recaptcha.url, { data: { secret: config.recaptcha.secret, response: this["g-recaptcha-response"] }})
			.then(result => {
				this.spam = !result.success;
				next();
			})
			.catch(err => {
				console.error(err);
				this.spam = true;
				next();
			})
		} else {
			this.spam = true;
			next();
		}
	})

});

module.exports = mongoose.model('Lead', LeadSchema);
<commit_msg>Hot, mild and cold leads<commit_after>var mongoose     = require('mongoose');
var Schema       = mongoose.Schema;

var ObjectId = mongoose.Schema.Types.ObjectId;
var Location = require('./location_model');
var Space = require('./space_model');
var User = require('./user_model');
var rest = require("restler-q");
var config = require("config");

var LeadSchema   = new Schema({
	name: { type: String, index: true },
	organisation: String,
	location_id: { type: ObjectId, index: true, ref: "Location" },
	space_id: { type: ObjectId, index: true, ref: "Space" },
	email: { type: String, index: true },
	mobile: String,
	date_created: { type: Date, default: Date.now },
	source: String,
	url: String,
	type: String,
	intercom_id: String,
	mailtemplate_id: ObjectId,
	membership_id: { type: ObjectId, ref: "Membership" },
	short_name: String,
	legal_name: String,
	accounts_email: String,
	website: String,
	address: String,
	postal_address: String,
	vat: String,
	company_registration_number: String,
	seats: Number,
	spam: { type: Boolean, default: false },
	data: mongoose.Schema.Types.Mixed,
	"g-recaptcha-response": String,
	referral_user_id: { type: ObjectId, ref: "User" },
	referral_date_paid: Date,
	referral_amount: Number,
	heat: { type: String, validate: /hot|mild|cold/ },
	_deleted: { type: Boolean, default: false, index: true },
	_owner_id: ObjectId
});

LeadSchema.set("_perms", {
	admin: "cru",
	super_user: "crud",
	user: "cr",
	all: "c",
});

LeadSchema.pre("save", function(next) {
	var Lead = require("./lead_model");
	var transaction = this;
	Lead.findOne({ _id: transaction._id })
	.then(result => {
		if (result) // Is an edit
			return next();
		if (this.__user) {
			this.spam = false;
			return next();
		}
		if (this["g-recaptcha-response"]) {
			rest.post(config.recaptcha.url, { data: { secret: config.recaptcha.secret, response: this["g-recaptcha-response"] }})
			.then(result => {
				this.spam = !result.success;
				next();
			})
			.catch(err => {
				console.error(err);
				this.spam = true;
				next();
			})
		} else {
			this.spam = true;
			next();
		}
	})

});

module.exports = mongoose.model('Lead', LeadSchema);
<|endoftext|>