<commit_before>import { action, computed, observable } from 'mobx'
import { encodeSource, decodeSource } from 'libs'
import { isEqual } from 'lodash'

const key = 'popup'
const defaultSource = '// Here You can type your custom JavaScript...'

export default class AppStore {
  constructor (store) {
    this.store = store
  }

  @observable autoSaveHandle = null
  @observable saved = false

  @observable hosts = []

  @observable enable = false
  @observable source = defaultSource

  @observable draft = null
  @observable truth = null

  @observable tab = { url: '' }
  @observable host = ''
  @observable protocol = ''

  @computed
  get include () {
    return this.store.IncludeStore.include
  }

  @computed
  get extra () {
    return this.store.IncludeStore.extra
  }

  @computed
  get domain () {
    return `${this.protocol}//${this.host}`
  }

  @computed
  get differentURL () {
    return !this.tab.url.startsWith(this.domain)
  }

  @computed
  get tabMode () {
    return this.tab.url === window.location.href
  }

  @computed
  get customjs () {
    return {
      config: {
        enable: this.enable,
        include: this.include,
        extra: this.extra
      },
      source: encodeSource(this.source)
    }
  }

  @computed
  get domainKey () {
    return `${key}-${this.domain}`
  }

  @action
  init = (domain) => {
    chrome.runtime.sendMessage({ method: 'getData', domain }, (response) => {
      if (!response || typeof response.host !== 'string') {
        throw new Error('Get no data for active tab!')
      }

      const { customjs, host, protocol, tab } = response
      Object.assign(this, {
        truth: customjs,
        host,
        protocol,
        tab
      })

      this.loadLocalStorage()

      const hostExist = this.hosts.includes(this.domain)
      if (!hostExist) {
        this.hosts.push(this.domain)
        this.saveHosts()
      }

      if (isEqual(this.draft, this.truth)) {
        this.draft = null
      }
      this.loadCustomjs(this.draft || this.truth)
    })
  }

  saveHosts = (hosts = this.hosts) => {
    window.localStorage.setItem(key, JSON.stringify({ hosts }))
  }

  loadCustomjs = (customjs = { config: {} }) => {
    const {
      source = defaultSource,
      config: {
        enable = false,
        include = '',
        extra = ''
      }
    } = customjs
    Object.assign(this, { enable, source: decodeSource(source) })
    Object.assign(this.store.IncludeStore, { include, extra })
  }

  loadLocalStorage = () => {
    const { draft } = JSON.parse(window.localStorage.getItem(this.domainKey) || '{}')
    const { hosts } = JSON.parse(window.localStorage.getItem(key) || '{}')
    Object.assign(this, { draft, hosts })
  }

  @action
  saveDraft = () => {
    this.draft = this.customjs
    window.localStorage.setItem(
      this.domainKey,
      JSON.stringify({ draft: this.draft })
    )
    this.saved = true
    this.autoSaveHandle = null
  }

  @action
  removeDraft = () => {
    this.draft = null
    this.saved = false
    window.localStorage.removeItem(this.domainKey)
  }

  @action
  onChangeSource = (value) => {
    this.source = value
    if (!this.enable) {
      this.enable = true
    }
    this.autoSave()
  }

  @action
  onHostChange = (newHost) => {
    this.init(newHost)
  }

  @action
  onRemoveDraft = () => {
    this.removeDraft()
    this.loadCustomjs(this.truth)
  }

  @action
  toggleEnable = () => {
    this.enable = !this.enable
    this.autoSave()
  }

  @action
  save = () => {
    this.removeDraft()
    const { domain, customjs } = this
    chrome.runtime.sendMessage({
      method: 'setData',
      domain,
      customjs,
      reload: true
    })
  }

  @action
  reset = () => {
    // TODO: confirm doesn't work with popup window
    this.loadCustomjs()
    chrome.runtime.sendMessage({
      method: 'removeData',
      domain: this.domain,
      reload: true
    })
    const newHosts = this.hosts.filter(x => x !== this.domain)
    this.saveHosts(newHosts)
    this.removeDraft()
  }

  @action
  goTo = () => {
    chrome.runtime.sendMessage({ method: 'goTo', link: this.domain })
  }

  autoSave = () => {
    if (this.autoSaveHandle) {
      clearTimeout(this.autoSaveHandle)
    }
    this.autoSaveHandle = setTimeout(this.saveDraft, 500)
  }
}
<commit_msg>Optimize bundlue size by import only lodash.isequal<commit_after>import { action, computed, observable } from 'mobx'
import { encodeSource, decodeSource } from 'libs'
import isEqual from 'lodash.isequal'

const key = 'popup'
const defaultSource = '// Here You can type your custom JavaScript...'

export default class AppStore {
  constructor (store) {
    this.store = store
  }

  @observable autoSaveHandle = null
  @observable saved = false

  @observable hosts = []

  @observable enable = false
  @observable source = defaultSource

  @observable draft = null
  @observable truth = null

  @observable tab = { url: '' }
  @observable host = ''
  @observable protocol = ''

  @computed
  get include () {
    return this.store.IncludeStore.include
  }

  @computed
  get extra () {
    return this.store.IncludeStore.extra
  }

  @computed
  get domain () {
    return `${this.protocol}//${this.host}`
  }

  @computed
  get differentURL () {
    return !this.tab.url.startsWith(this.domain)
  }

  @computed
  get tabMode () {
    return this.tab.url === window.location.href
  }

  @computed
  get customjs () {
    return {
      config: {
        enable: this.enable,
        include: this.include,
        extra: this.extra
      },
      source: encodeSource(this.source)
    }
  }

  @computed
  get domainKey () {
    return `${key}-${this.domain}`
  }

  @action
  init = (domain) => {
    chrome.runtime.sendMessage({ method: 'getData', domain }, (response) => {
      if (!response || typeof response.host !== 'string') {
        throw new Error('Get no data for active tab!')
      }

      const { customjs, host, protocol, tab } = response
      Object.assign(this, {
        truth: customjs,
        host,
        protocol,
        tab
      })

      this.loadLocalStorage()

      const hostExist = this.hosts.includes(this.domain)
      if (!hostExist) {
        this.hosts.push(this.domain)
        this.saveHosts()
      }

      if (isEqual(this.draft, this.truth)) {
        this.draft = null
      }
      this.loadCustomjs(this.draft || this.truth)
    })
  }

  saveHosts = (hosts = this.hosts) => {
    window.localStorage.setItem(key, JSON.stringify({ hosts }))
  }

  loadCustomjs = (customjs = { config: {} }) => {
    const {
      source = defaultSource,
      config: {
        enable = false,
        include = '',
        extra = ''
      }
    } = customjs
    Object.assign(this, { enable, source: decodeSource(source) })
    Object.assign(this.store.IncludeStore, { include, extra })
  }

  loadLocalStorage = () => {
    const { draft } = JSON.parse(window.localStorage.getItem(this.domainKey) || '{}')
    const { hosts } = JSON.parse(window.localStorage.getItem(key) || '{}')
    Object.assign(this, { draft, hosts })
  }

  @action
  saveDraft = () => {
    this.draft = this.customjs
    window.localStorage.setItem(
      this.domainKey,
      JSON.stringify({ draft: this.draft })
    )
    this.saved = true
    this.autoSaveHandle = null
  }

  @action
  removeDraft = () => {
    this.draft = null
    this.saved = false
    window.localStorage.removeItem(this.domainKey)
  }

  @action
  onChangeSource = (value) => {
    this.source = value
    if (!this.enable) {
      this.enable = true
    }
    this.autoSave()
  }

  @action
  onHostChange = (newHost) => {
    this.init(newHost)
  }

  @action
  onRemoveDraft = () => {
    this.removeDraft()
    this.loadCustomjs(this.truth)
  }

  @action
  toggleEnable = () => {
    this.enable = !this.enable
    this.autoSave()
  }

  @action
  save = () => {
    this.removeDraft()
    const { domain, customjs } = this
    chrome.runtime.sendMessage({
      method: 'setData',
      domain,
      customjs,
      reload: true
    })
  }

  @action
  reset = () => {
    // TODO: confirm doesn't work with popup window
    this.loadCustomjs()
    chrome.runtime.sendMessage({
      method: 'removeData',
      domain: this.domain,
      reload: true
    })
    const newHosts = this.hosts.filter(x => x !== this.domain)
    this.saveHosts(newHosts)
    this.removeDraft()
  }

  @action
  goTo = () => {
    chrome.runtime.sendMessage({ method: 'goTo', link: this.domain })
  }

  autoSave = () => {
    if (this.autoSaveHandle) {
      clearTimeout(this.autoSaveHandle)
    }
    this.autoSaveHandle = setTimeout(this.saveDraft, 500)
  }
}
<|endoftext|>


<commit_before>import Job from './Job';
import Tree from './Tree';

module.exports = class JobTree extends Tree {
  /**
   * (Chronos) JobTree
   * @param {{
   *          id:string,
   *          items:array<({id:string, items:array}|*)>,
   *          filterProperties:{propertyName:(null|string|function)}
   *        }} options
   * TODO: DCOS-7369 - Add remove item method
   * @constructor
   * @struct
   */
  constructor({id, items, filterProperties} = {}) {
    super({filterProperties});

    this.id = '/';

    if (typeof id == 'string') {
      if (id !== '/' && (!id.startsWith('/') || id.endsWith('/'))) {
        throw new Error(`Id (${id}) must start with a leading slash ("/") ` +
          'and should not end with a slash, except for root id which is only ' +
          'a slash.');
      }

      this.id = id;
    }

    // Parse and add items to the current tree
    if (Array.isArray(items)) {
      items.forEach((item)=> {
        // Create instances of JobTree for tree like items and add them
        if ((item.items != null && Array.isArray(item.items))
          && !(item instanceof JobTree)) {
          this.add(new this.constructor(
            Object.assign({filterProperties: this.getFilterProperties()}, item)
          ));

          return;
        }

        // Convert items into instance of Job and add them to the current tree
        if (!(item instanceof Job) && !(item instanceof JobTree)) {
          this.add(new Job(item));

          return;
        }

        this.add(item);
      })
    }
  }

  /**
   * Add item - This method will create sub trees if needed to insert the item
   * at the correct location (based on id/path matching).
   * @param {JobTree|Job} item
   */
  add(item) {
    if (!(item instanceof Job || item instanceof JobTree)) {
      throw new TypeError('item is neither an instance of Job nor JobTree');
    }

    const itemId = item.getId();

    if (!itemId.startsWith(this.getId())) {
      throw new Error(
        `item id (${itemId}) doesn't match tree id (${this.getId()})`
      );
    }

    // Check if the item is already present in the tree (including sub trees)
    if (this.findItemById(itemId)) {
      // TODO: DCOS-7370 - Update item (type Job) data
      return;
    }

    // Get the parent id (e.g. /group) by matching every thing but the item
    // name including the preceding slash "/" (e.g. /id).
    const [parentId] = itemId.match(/\/.*?(?=\/?[^/]+\/?$)/);

    // Add item to the current tree if it's the actual parent tree
    if (this.getId() === parentId) {
      super.add(item);
      return;
    }

    // Find or create corresponding parent tree and add it to the tree
    let parent = this.findItemById(parentId);
    if (!parent) {
      parent = new this.constructor({id: parentId});
      this.add(parent);
    }

    // Add item to parent tree
    parent.add(item);
  }

  getId() {
    return this.id;
  }

  /**
   * @param {string} id
   * @return {Job|JobTree} matching item
   */
  findItemById(id) {
    return this.findItem(function (item) {
      return item.getId() === id;
    });
  }

  getName() {
    return this.getId().split('/').pop();
  }
};
<commit_msg>Stylistic adjustments<commit_after>import Job from './Job';
import Tree from './Tree';

module.exports = class JobTree extends Tree {
  /**
   * (Chronos) JobTree
   * @param {{
   *          id:string,
   *          items:array<({id:string, items:array}|*)>,
   *          filterProperties:{propertyName:(null|string|function)}
   *        }} options
   * TODO: DCOS-7369 - Add remove item method
   * @constructor
   * @struct
   */
  constructor({id, items, filterProperties} = {}) {
    super({filterProperties});

    this.id = '/';

    if (typeof id == 'string') {
      if (id !== '/' && (!id.startsWith('/') || id.endsWith('/'))) {
        throw new Error(`Id (${id}) must start with a leading slash ("/") ` +
          'and should not end with a slash, except for root id which is only ' +
          'a slash.');
      }

      this.id = id;
    }

    // Parse and add items to the current tree
    if (Array.isArray(items)) {
      items.forEach((item) => {
        // Create instances of JobTree for tree like items and add them
        if ((item.items != null && Array.isArray(item.items))
          && !(item instanceof JobTree)) {
          this.add(new this.constructor(
            Object.assign({filterProperties: this.getFilterProperties()}, item)
          ));

          return;
        }

        // Convert items into instance of Job and add them to the current tree
        if (!(item instanceof Job) && !(item instanceof JobTree)) {
          this.add(new Job(item));

          return;
        }

        this.add(item);
      })
    }
  }

  /**
   * Add item - This method will create sub trees if needed to insert the item
   * at the correct location (based on id/path matching).
   * @param {JobTree|Job} item
   */
  add(item) {
    if (!(item instanceof Job || item instanceof JobTree)) {
      throw new TypeError('item is neither an instance of Job nor JobTree');
    }

    const id = this.getId();
    const itemId = item.getId();

    if (!itemId.startsWith(id)) {
      throw new Error(
        `item id (${itemId}) doesn't match tree id (${id})`
      );
    }

    // Check if the item is already present in the tree (including sub trees)
    if (this.findItemById(itemId)) {
      // TODO: DCOS-7370 - Update item (type Job) data
      return;
    }

    // Get the parent id (e.g. /group) by matching every thing but the item
    // name including the preceding slash "/" (e.g. /id).
    const [parentId] = itemId.match(/\/.*?(?=\/?[^/]+\/?$)/);

    // Add item to the current tree if it's the actual parent tree
    if (id === parentId) {
      super.add(item);
      return;
    }

    // Find or create corresponding parent tree and add it to the tree
    let parent = this.findItemById(parentId);
    if (!parent) {
      parent = new this.constructor({id: parentId});
      this.add(parent);
    }

    // Add item to parent tree
    parent.add(item);
  }

  getId() {
    return this.id;
  }

  /**
   * @param {string} id
   * @return {Job|JobTree} matching item
   */
  findItemById(id) {
    return this.findItem(function (item) {
      return item.getId() === id;
    });
  }

  getName() {
    return this.getId().split('/').pop();
  }
};
<|endoftext|>


<commit_before>//////// Module Definition ////////////
define([
	'util/util', // pulls in angular
	'util/wikidataapi',
	'app/statistics',
	'app/properties',
	'util/dataFormatter',
	'util/i18n'
], function() {
///////////////////////////////////////

angular.module('utilities').directive('sqidImage', ['wikidataapi', function(wikidataapi) {

	var link = function (scope, element, attrs) {
		scope.$watch(attrs.file, function(file){
			wikidataapi.getImageData(file,attrs.width).then(function(imagedata) {
				var html = '' +
						'' +
					'';
				element.replaceWith(html);
			});
		});
	};

	return {
		restrict: 'E',
		link: link
	};
}])

.directive('sqidFooter', ['$compile', 'statistics', function($compile, statistics) {

	var link = function (scope, element, attrs) {
		statistics.then(function(stats) {
			var innerHtml = '
 ()
';
			innerHtml += '

';
			element.html('

' + innerHtml + '

');
			$compile(element.contents())(scope);
		});
	};

	return {
		restrict: 'E',
		link: link
	};
}])

/**
 * Directive to include trusted or untrusted HTML snippets and compiling
 * the result. This is necessary to expand directives within the snippets,
 * which does not work when using ng-bind-html-trusted to incude HTML.
 */
.directive('sqidCompile', ['$compile', function ($compile) {
	return function(scope, element, attrs) {
		scope.$watch(
			function(scope) {
				return scope.$eval(attrs.sqidCompile);
			},
			function(value) {
				// If value is a TrustedValueHolderType, it needs to be
				// explicitly converted to a string in order to
				// get the HTML string.
				element.html(value && value.toString());
				$compile(element.contents())(scope);
			}
		);
	};
}])

.directive('sqidStatementTable', ['$compile', 'Properties', 'dataFormatter', 'util', 'i18n', function($compile, Properties, dataFormatter, util, i18n) {
	var properties = null;
	var outMissingTermsListener = { hasMissingTerms : false};
	var inMissingTermsListener = { hasMissingTerms : false};
	var outHtml = '';
	var inHtml = '';

	var hideStatementsThreshold = 3; // how many statements are displayed when hiding some

	var link = function (scope, element, attrs) {
		var show = attrs.show;
		var title = attrs.title;
		var narrowTable = ('narrow' in attrs);

		outHtml = '';
		inHtml = '';
		outMissingTermsListener.hasMissingTerms = false;
		inMissingTermsListener.hasMissingTerms = false;

		var includeProperty = function(numId) {
			if (!show || show == 'all') {
				return true;
			}
			if (numId == '31' || numId == '279') {
				return false;
			}

			var isId = (properties.getDatatype(numId) == 'ExternalId');
			var isHumanRelation = false;
			var isMedia = (properties.getDatatype(numId) == 'CommonsMedia');
			var isAboutWikiPages = (numId == '1151') || // "topic's main Wikimedia portal"
									(numId == '910'); // "topic's main category"
			angular.forEach(properties.getClasses(numId), function(classId) {
				if (classId == '19847637' || // "Wikidata property representing a unique identifier"
					classId == '18614948' || // "Wikidata property for authority control"
					classId == '19595382' || // "Wikidata property for authority control for people"
					classId == '19829908' || // "Wikidata property for authority control for places"
					classId == '19833377' || // "Wikidata property for authority control for works"
					classId == '18618628' || // "Wikidata property for authority control for cultural heritage identification"
					classId == '21745557' || // "Wikidata property for authority control for organisations"
					classId == '19833835' || // "Wikidata property for authority control for substances"
					classId == '22964274'  // "Wikidata property for identication in the film industry"
				) {
					isId = true;
				} else if (classId == '22964231') { // "Wikidata property for human relationships"
					isHumanRelation = true;
				} else if (classId == '18610173') { // "Wikidata property for Commons"
					isMedia = true;
				} else if (classId == '18667213' || // "Wikidata property about Wikimedia categories"
					classId == '22969221' // "Wikidata property giving Wikimedia list"
				) {
					isAboutWikiPages = true;
				}
			});

			switch (show) {
				case 'ids':
					return isId;
				case 'family':
					return isHumanRelation;
				case 'media':
					return isMedia;
				case 'wiki':
					return isAboutWikiPages;
				case 'other': default:
					return !isId && !isHumanRelation && !isMedia && !isAboutWikiPages;
			}
		}

		var getStatementHtmlTable = function(statements, propertyList, outlinks) {
			var html = '

';
			var hasContent = false;
			var missingTermsListener = outlinks ? outMissingTermsListener : inMissingTermsListener;

			angular.forEach(propertyList, function (propId) {
				var statementGroup = statements[propId]
				var propertyLabel = i18n.getPropertyLabel(propId);

				var hideSomeStatements = (statementGroup.length > hideStatementsThreshold + 1);
				angular.forEach(statementGroup, function (statement, index) {
					hasContent = true;
					if (hideSomeStatements && index >= hideStatementsThreshold) {
						html += '
';
					} else {
						html += '
';
					}
					if (index == 0) {
						html += ''
							+ i18n.getPropertyLink(propId)
							+ (hideSomeStatements ? '

' : '')
							+ '	';
					}

					if (outlinks) { // expand statement, only used for outlinks right now (may change in future)
						html += '' +
							'

'
					} else {
						html += '';
					}

					if (!outlinks) { // show left-arrow for inlinks
						html += '';
					}
					html += dataFormatter.getStatementValueBlockHtml(statement, properties, missingTermsListener, outlinks, narrowTable) +
						'
';
				});
			});
			if (!hasContent) return '';
			html += '

';
			return html;
		}

		var updateHtml = function(element, scope) {
			var html;
			if ( scope.outHtml == '' && scope.inHtml == '' ) {
				html = '';
			} else {
				var panelId = 'statements_' + show;
				html = '
\n' +
							'

\n' +
							'' + title + '

' +
							'
' +
							'
';
				if (scope.inHtml != '') {
					html += '<uib-tabset type="pills" justified="true">' +
						'<uib-tab classes="shy-pill"><uib-tab-heading></uib-tab-heading>' +
						scope.outHtml +
						'</uib-tab>' +
						'<uib-tab classes="shy-pill"><uib-tab-heading></uib-tab-heading>' +
						scope.inHtml +
						'</uib-tab>' +
						'</uib-tabset>';
				} else { // outHtml != '' in this case
					html += scope.outHtml;
				}
				html += '

';
			}
			insertAndCompile(html, element, scope);
		}

		var preparePropertyList = function(statements) {
			propertyScores = {};
			// Note: class-based ranking rarely seems to help; hence using properties only
			for (propertyId in statements) {
				angular.forEach(properties.getRelatedProperties(propertyId.substring(1)), function(relPropScore, relPropId) {
					if (relPropId in propertyScores) {
						propertyScores[relPropId] = propertyScores[relPropId] + relPropScore;
					} else {
						propertyScores[relPropId] = relPropScore;
					}
				});
			}

			scoredProperties = [];

			for (propertyId in statements) {
				var numPropId = propertyId.substring(1);
				if (includeProperty(numPropId)) {
					if (numPropId in propertyScores) {
						scoredProperties.push([numPropId,propertyScores[numPropId]]);
					} else {
						scoredProperties.push([numPropId,0]);
					}
				}
			}

			scoredProperties.sort(function(a, b) {
				var a = a[1];
				var b = b[1];
				return a  b ? -1 : 0);
			});

			ret = [];
			angular.forEach(scoredProperties, function(propertyData) {
				ret.push('P' + propertyData[0]);
			});
			return ret;
		}

		var insertAndCompile = function(html, element, scope) {
			element.html(html);
			$compile(element.contents())(scope);
		}

		// Register additional functions to toggle the display for longer lists of statements:
		scope.showRows = function(id) {
			var field = 'show' + id;
			if (field in scope) {
				return scope[field];
			} else {
				return false;
			}
		}
		scope.toggleRows = function(id) {
			var field = 'show' + id;
			scope[field] = !scope.showRows(id);
		}
		scope.getRowSpan = function(id,length) {
			return scope.showRows(id) ? length : hideStatementsThreshold;
		}
		scope.getShowRowsMessage = function(id) {
			return scope.showRows(id) ? 'STATEMENTS.LESS_STATEMENTS' : 'STATEMENTS.MORE_STATEMENTS';
		}
		scope.getShowRowsClass = function(id) {
			return (scope.showRows(id) ? 'expand-open' : 'expand-closed' );
		}

		scope.inHtml = '';
		scope.outHtml = '';

		scope.$watch(attrs.data, function(itemData){
			itemData.waitForPropertyLabels().then(function() {
				Properties.then(function(propertyData){
					properties = propertyData;
					var propertyList = preparePropertyList(itemData.statements);

					scope.outHtml = getStatementHtmlTable(itemData.statements, propertyList, true);
					updateHtml(element, scope);
					if (outMissingTermsListener.hasMissingTerms) {
						itemData.waitForTerms().then( function() {
							outMissingTermsListener.hasMissingTerms = false;
							scope.outHtml = getStatementHtmlTable(itemData.statements, propertyList, true);
							updateHtml(element, scope);
						});
					}
				})
			});
		});

		scope.$watch(attrs.indata, function(inData){
			if (!inData) return;
			inData.waitForPropertyLabels().then(function() {
				Properties.then(function(propertyData){
					properties = propertyData;
					var propertyList = preparePropertyList(inData.statements);

					var inHtml = getStatementHtmlTable(inData.statements, propertyList, false);
					if (inMissingTermsListener.hasMissingTerms) {
						inData.waitForTerms().then( function() {
							inMissingTermsListener.hasMissingTerms = false;
							scope.inHtml = getStatementHtmlTable(inData.statements, propertyList, false);
							updateHtml(element, scope);
						});
					} else {
						scope.inHtml = inHtml;
						updateHtml(element, scope);
					}
				})
			});
		});
	}

	return {
		restrict: 'E',
		link: link
	};
}]);

return {}; // module
});		  // definition end<commit_msg>fixed bug in expanding statement lists<commit_after>//////// Module Definition ////////////
define([
	'util/util', // pulls in angular
	'util/wikidataapi',
	'app/statistics',
	'app/properties',
	'util/dataFormatter',
	'util/i18n'
], function() {
///////////////////////////////////////

angular.module('utilities').directive('sqidImage', ['wikidataapi', function(wikidataapi) {

	var link = function (scope, element, attrs) {
		scope.$watch(attrs.file, function(file){
			wikidataapi.getImageData(file,attrs.width).then(function(imagedata) {
				var html = '' +
						'' +
					'';
				element.replaceWith(html);
			});
		});
	};

	return {
		restrict: 'E',
		link: link
	};
}])

.directive('sqidFooter', ['$compile', 'statistics', function($compile, statistics) {

	var link = function (scope, element, attrs) {
		statistics.then(function(stats) {
			var innerHtml = '
 ()
';
			innerHtml += '

';
			element.html('

' + innerHtml + '

');
			$compile(element.contents())(scope);
		});
	};

	return {
		restrict: 'E',
		link: link
	};
}])

/**
 * Directive to include trusted or untrusted HTML snippets and compiling
 * the result. This is necessary to expand directives within the snippets,
 * which does not work when using ng-bind-html-trusted to incude HTML.
 */
.directive('sqidCompile', ['$compile', function ($compile) {
	return function(scope, element, attrs) {
		scope.$watch(
			function(scope) {
				return scope.$eval(attrs.sqidCompile);
			},
			function(value) {
				// If value is a TrustedValueHolderType, it needs to be
				// explicitly converted to a string in order to
				// get the HTML string.
				element.html(value && value.toString());
				$compile(element.contents())(scope);
			}
		);
	};
}])

.directive('sqidStatementTable', ['$compile', 'Properties', 'dataFormatter', 'util', 'i18n', function($compile, Properties, dataFormatter, util, i18n) {
	var properties = null;
	var outMissingTermsListener = { hasMissingTerms : false};
	var inMissingTermsListener = { hasMissingTerms : false};
	var outHtml = '';
	var inHtml = '';

	var hideStatementsThreshold = 3; // how many statements are displayed when hiding some

	var link = function (scope, element, attrs) {
		var show = attrs.show;
		var title = attrs.title;
		var narrowTable = ('narrow' in attrs);

		outHtml = '';
		inHtml = '';
		outMissingTermsListener.hasMissingTerms = false;
		inMissingTermsListener.hasMissingTerms = false;

		var includeProperty = function(numId) {
			if (!show || show == 'all') {
				return true;
			}
			if (numId == '31' || numId == '279') {
				return false;
			}

			var isId = (properties.getDatatype(numId) == 'ExternalId');
			var isHumanRelation = false;
			var isMedia = (properties.getDatatype(numId) == 'CommonsMedia');
			var isAboutWikiPages = (numId == '1151') || // "topic's main Wikimedia portal"
									(numId == '910'); // "topic's main category"
			angular.forEach(properties.getClasses(numId), function(classId) {
				if (classId == '19847637' || // "Wikidata property representing a unique identifier"
					classId == '18614948' || // "Wikidata property for authority control"
					classId == '19595382' || // "Wikidata property for authority control for people"
					classId == '19829908' || // "Wikidata property for authority control for places"
					classId == '19833377' || // "Wikidata property for authority control for works"
					classId == '18618628' || // "Wikidata property for authority control for cultural heritage identification"
					classId == '21745557' || // "Wikidata property for authority control for organisations"
					classId == '19833835' || // "Wikidata property for authority control for substances"
					classId == '22964274'  // "Wikidata property for identication in the film industry"
				) {
					isId = true;
				} else if (classId == '22964231') { // "Wikidata property for human relationships"
					isHumanRelation = true;
				} else if (classId == '18610173') { // "Wikidata property for Commons"
					isMedia = true;
				} else if (classId == '18667213' || // "Wikidata property about Wikimedia categories"
					classId == '22969221' // "Wikidata property giving Wikimedia list"
				) {
					isAboutWikiPages = true;
				}
			});

			switch (show) {
				case 'ids':
					return isId;
				case 'family':
					return isHumanRelation;
				case 'media':
					return isMedia;
				case 'wiki':
					return isAboutWikiPages;
				case 'other': default:
					return !isId && !isHumanRelation && !isMedia && !isAboutWikiPages;
			}
		}

		var getStatementHtmlTable = function(statements, propertyList, outlinks) {
			var html = '

';
			var hasContent = false;
			var missingTermsListener = outlinks ? outMissingTermsListener : inMissingTermsListener;

			angular.forEach(propertyList, function (propId) {
				var statementListId = propId + ( outlinks ? '-out' : '-in' );
				var statementGroup = statements[propId]
				var propertyLabel = i18n.getPropertyLabel(propId);

				var hideSomeStatements = (statementGroup.length > hideStatementsThreshold + 1);
				angular.forEach(statementGroup, function (statement, index) {
					hasContent = true;
					if (hideSomeStatements && index >= hideStatementsThreshold) {
						html += '
';
					} else {
						html += '
';
					}
					if (index == 0) {
						html += ''
							+ i18n.getPropertyLink(propId)
							+ (hideSomeStatements ? '

' : '')
							+ '	';
					}

					if (outlinks) { // expand statement, only used for outlinks right now (may change in future)
						html += '' +
							'

'
					} else {
						html += '';
					}

					if (!outlinks) { // show left-arrow for inlinks
						html += '';
					}
					html += dataFormatter.getStatementValueBlockHtml(statement, properties, missingTermsListener, outlinks, narrowTable) +
						'
';
				});
			});
			if (!hasContent) return '';
			html += '

';
			return html;
		}

		var updateHtml = function(element, scope) {
			var html;
			if ( scope.outHtml == '' && scope.inHtml == '' ) {
				html = '';
			} else {
				var panelId = 'statements_' + show;
				html = '
\n' +
							'

\n' +
							'' + title + '

' +
							'
' +
							'
';
				if (scope.inHtml != '') {
					html += '<uib-tabset type="pills" justified="true">' +
						'<uib-tab classes="shy-pill"><uib-tab-heading></uib-tab-heading>' +
						scope.outHtml +
						'</uib-tab>' +
						'<uib-tab classes="shy-pill"><uib-tab-heading></uib-tab-heading>' +
						scope.inHtml +
						'</uib-tab>' +
						'</uib-tabset>';
				} else { // outHtml != '' in this case
					html += scope.outHtml;
				}
				html += '

';
			}
			insertAndCompile(html, element, scope);
		}

		var preparePropertyList = function(statements) {
			propertyScores = {};
			// Note: class-based ranking rarely seems to help; hence using properties only
			for (propertyId in statements) {
				angular.forEach(properties.getRelatedProperties(propertyId.substring(1)), function(relPropScore, relPropId) {
					if (relPropId in propertyScores) {
						propertyScores[relPropId] = propertyScores[relPropId] + relPropScore;
					} else {
						propertyScores[relPropId] = relPropScore;
					}
				});
			}

			scoredProperties = [];

			for (propertyId in statements) {
				var numPropId = propertyId.substring(1);
				if (includeProperty(numPropId)) {
					if (numPropId in propertyScores) {
						scoredProperties.push([numPropId,propertyScores[numPropId]]);
					} else {
						scoredProperties.push([numPropId,0]);
					}
				}
			}

			scoredProperties.sort(function(a, b) {
				var a = a[1];
				var b = b[1];
				return a  b ? -1 : 0);
			});

			ret = [];
			angular.forEach(scoredProperties, function(propertyData) {
				ret.push('P' + propertyData[0]);
			});
			return ret;
		}

		var insertAndCompile = function(html, element, scope) {
			element.html(html);
			$compile(element.contents())(scope);
		}

		// Register additional functions to toggle the display for longer lists of statements:
		scope.showRows = function(id) {
			var field = 'show' + id;
			if (field in scope) {
				return scope[field];
			} else {
				return false;
			}
		}
		scope.toggleRows = function(id) {
			var field = 'show' + id;
			scope[field] = !scope.showRows(id);
		}
		scope.getRowSpan = function(id,length) {
			return scope.showRows(id) ? length : hideStatementsThreshold;
		}
		scope.getShowRowsMessage = function(id) {
			return scope.showRows(id) ? 'STATEMENTS.LESS_STATEMENTS' : 'STATEMENTS.MORE_STATEMENTS';
		}
		scope.getShowRowsClass = function(id) {
			return (scope.showRows(id) ? 'expand-open' : 'expand-closed' );
		}

		scope.inHtml = '';
		scope.outHtml = '';

		scope.$watch(attrs.data, function(itemData){
			itemData.waitForPropertyLabels().then(function() {
				Properties.then(function(propertyData){
					properties = propertyData;
					var propertyList = preparePropertyList(itemData.statements);

					scope.outHtml = getStatementHtmlTable(itemData.statements, propertyList, true);
					updateHtml(element, scope);
					if (outMissingTermsListener.hasMissingTerms) {
						itemData.waitForTerms().then( function() {
							outMissingTermsListener.hasMissingTerms = false;
							scope.outHtml = getStatementHtmlTable(itemData.statements, propertyList, true);
							updateHtml(element, scope);
						});
					}
				})
			});
		});

		scope.$watch(attrs.indata, function(inData){
			if (!inData) return;
			inData.waitForPropertyLabels().then(function() {
				Properties.then(function(propertyData){
					properties = propertyData;
					var propertyList = preparePropertyList(inData.statements);

					var inHtml = getStatementHtmlTable(inData.statements, propertyList, false);
					if (inMissingTermsListener.hasMissingTerms) {
						inData.waitForTerms().then( function() {
							inMissingTermsListener.hasMissingTerms = false;
							scope.inHtml = getStatementHtmlTable(inData.statements, propertyList, false);
							updateHtml(element, scope);
						});
					} else {
						scope.inHtml = inHtml;
						updateHtml(element, scope);
					}
				})
			});
		});
	}

	return {
		restrict: 'E',
		link: link
	};
}]);

return {}; // module
});		  // definition end<|endoftext|>


<commit_before>// (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP

import CSSClassnames from './CSSClassnames';

const CLASS_ROOT = CSSClassnames.APP;

function clearAnnouncer() {
  const announcer = document.querySelector(`.${CLASS_ROOT}__announcer`);
  announcer.innerHTML = '';
};

export function announcePageLoaded (title) {
  announce(`${title} page was loaded`);
}

export function announce (message, mode = 'assertive') {
  const announcer = document.querySelector(`.${CLASS_ROOT}__announcer`);
  announcer.setAttribute('aria-live', 'off');
  announcer.innerHTML = message;
  setTimeout(clearAnnouncer, 500);
  announcer.setAttribute('aria-live', mode);
}

export default { announce, announcePageLoaded };
<commit_msg>Fix exceptions when using Toast without having App<commit_after>// (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP

import CSSClassnames from './CSSClassnames';

const CLASS_ROOT = CSSClassnames.APP;

function clearAnnouncer() {
  const announcer = document.querySelector(`.${CLASS_ROOT}__announcer`);
  if(announcer) {
    announcer.innerHTML = '';
  }
};

export function announcePageLoaded (title) {
  announce(`${title} page was loaded`);
}

export function announce (message, mode = 'assertive') {
  const announcer = document.querySelector(`.${CLASS_ROOT}__announcer`);
  if(announcer) {
    announcer.setAttribute('aria-live', 'off');
    announcer.innerHTML = message;
    setTimeout(clearAnnouncer, 500);
    announcer.setAttribute('aria-live', mode);
  }
}

export default { announce, announcePageLoaded };
<|endoftext|>


<commit_before>import Ember from 'ember';
import DS from 'ember-data';
const { getOwner } = Ember;

var attr = DS.attr,
    hasMany = DS.hasMany,
    belongsTo = DS.belongsTo;

export default DS.Model.extend({
  language:       attr('string'),
  state:          attr('string', {defaultValue: 'draft'}),
  origin:         attr('string'),
  stairs:         attr('boolean'),
  parking:        attr('boolean'),
  estimatedSize:  attr('string'),
  notes:          attr('string'),
  createdAt:      attr('date'),
  updatedAt:      attr('date'),
  submittedAt:    attr('date'),
  cancelledAt:    attr('date'),
  state_event:    attr('string'),
  reviewedAt:     attr('date'),
  receivedAt:     attr('date'),
  reviewCompletedAt: attr('date'),
  deliveredBy:    attr('string'),
  startReceivingAt: attr('date'),
  cancelReason:   attr('string'),

  gogovanTransport:    belongsTo('gogovan_transport', { async: false }),
  crossroadsTransport: belongsTo('crossroads_transport', { async: false }),
  cancellationReason:  belongsTo('cancellation_reason', { async: false }),

  // used for items of current-offer
  saleable:       attr('boolean'),

  items:          hasMany('item', { async: false }),
  messages:       hasMany('message', { async: false }),

  delivery:       belongsTo('delivery', { async: false }),
  createdBy:      belongsTo('user', { async: false }),
  reviewedBy:     belongsTo('user', { async: false }),
  closedBy:       belongsTo('user', { async: false }),
  receivedBy:     belongsTo('user', { async: false }),

  // User details
  userName:       attr('string'),
  userPhone:      attr('string'),

  crossroadsTruckCost: Ember.computed('crossroadsTransport', function(){
    return this.get('crossroadsTransport.cost');
  }),

  itemCount: Ember.computed('items.@each.state', function(){
    return this.get("items").rejectBy("state", "draft").length;
  }),

  allPackages: Ember.computed(function(){
    return this.store.peekAll("package");
  }),

  packages: Ember.computed("allPackages.@each.offerId", function(){
    return this.get("allPackages").filterBy("offerId", parseInt(this.get("id")));
  }),

  itemPackages: Ember.computed.alias("packages"),

  receivedCount: Ember.computed("packages.@each.state", function(){
    return this.get('packages').filterBy("state", "received").length;
  }),

  missingCount: Ember.computed("packages.@each.state", function(){
    return this.get('packages').filterBy("state", "missing").length;
  }),

  approvedItems: Ember.computed.filterBy("items", "state", "accepted"),
  rejectedItems: Ember.computed.filterBy("items", "state", "rejected"),
  submittedItems: Ember.computed.filterBy("items", "state", "submitted"),
  isDraft: Ember.computed.equal("state", "draft"),
  isInactive: Ember.computed.equal("state", "inactive"),
  isSubmitted: Ember.computed.equal("state", "submitted"),
  isScheduled: Ember.computed.equal("state", "scheduled"),
  isUnderReview: Ember.computed.equal("state", "under_review"),
  isReviewed: Ember.computed.equal("state", "reviewed"),
  isClosed: Ember.computed.equal("state", "closed"),
  isReceived: Ember.computed.equal("state", "received"),
  isReceiving: Ember.computed.equal("state", "receiving"),
  isCancelled: Ember.computed.equal("state", "cancelled"),
  preventNewItem: Ember.computed.alias("isFinished"),

  hasReceived: Ember.computed.or('isReceived', 'isReceiving'),
  isReviewing: Ember.computed.or('isUnderReview', 'isReviewed'),
  adminCurrentOffer: Ember.computed.or('isReviewing', 'isScheduled'),
  nonSubmittedOffer: Ember.computed.or('isDraft', 'isInactive'),
  closedOrCancelled: Ember.computed.or('isClosed', 'isCancelled'),

  needReview: Ember.computed('isUnderReview', 'isSubmitted', 'isClosed', function(){
    return this.get('isUnderReview') || this.get('isSubmitted') || this.get("isClosed");
  }),

  isFinished: Ember.computed('isClosed', 'isReceived', 'isCancelled', 'isInactive', function(){
    return this.get('isClosed') || this.get('isReceived') || this.get('isCancelled') || this.get('isInactive');
  }),

  canMerged: Ember.computed('isSubmitted', 'isUnderReview', 'isReviewed', function(){
    return this.get('isSubmitted') || this.get('isUnderReview') || this.get('isReviewed');
  }),

  baseForMerge: Ember.computed('isSubmitted', 'isUnderReview', 'isReviewed', 'isScheduled', function(){
    return this.get('isSubmitted') || this.get('isUnderReview') || this.get('isReviewed') || this.get('isScheduled');
  }),

  activeItems: Ember.computed('items.@each.state', function(){
    return this.get('items').rejectBy("state", "draft");
  }),

  nonEmptyOffer: Ember.computed('items.[]', function(){
    return this.get('itemCount') > 0;
  }),

  allItemsReviewed: Ember.computed('items.@each.state', function(){
    var reviewedItems = this.get('activeItems').filterBy('state', 'submitted');
    return reviewedItems.get('length') === 0;
  }),

  readyForSchedule: Ember.computed('needReview', 'allItemsReviewed', function(){
    return this.get('needReview') && this.get('allItemsReviewed');
  }),

  allItemsRejected: Ember.computed('items.@each.state', 'needReview', function(){
    var rejectedItems = this.get('activeItems').filterBy('state', 'rejected');
    return this.get('needReview') && (rejectedItems.get('length') === this.get('itemCount'));
  }),

  displayImageUrl: Ember.computed('items.@each.displayImageUrl', function(){
    return this.get("activeItems.firstObject.displayImageUrl") || "assets/images/default_item.jpg";
  }),

  isCharitableSale: Ember.computed('items.@each.saleable', function(){
    var isSaleable = this.get("items").rejectBy("saleable", false).length > 0;
    return  isSaleable ? this.locale("yes") : this.locale("no");
  }),

  isAccepted: Ember.computed('items.@each.saleable', function(){
    return (this.get("approvedItems").length > 0) && this.get('isReviewed');
  }),

  status: Ember.computed('state', function(){
    var state = this.get('state');
    var status;
    switch(state) {
      case 'draft': return this.locale('offers.index.complete_offer');
      case 'under_review' : return this.locale('offers.index.in_review');
      case 'submitted' : return this.locale('offers.index.awaiting_review');
      case 'reviewed' : return this.locale('offers.index.arrange_transport');
      case 'scheduled' : return this.scheduledStatus();
      case 'closed' : return this.locale('offers.index.closed');
      case 'received' : return this.locale('offers.index.received');
      case 'receiving' : return this.locale('offers.index.receiving');
      case 'inactive' : return this.locale('offers.index.inactive');
    }
    return status;
  }),

  i18n: Ember.inject.service(),

  locale: function(text) {
    return this.get("i18n").t(text);
  },

  statusText: Ember.computed('status', 'itemCount', function(){
    return this.get("nonSubmittedOffer") ? this.get("status") : (this.get("status") + " ("+ this.get("itemCount") + " "+ this.locale("items_text") +")")
  }),

  scheduledStatus: function(){
    var deliveryType = this.get("delivery.deliveryType")
    switch(deliveryType) {
      case "Gogovan" : return this.get("gogovan_status");
      case "Drop Off" : return this.locale("offers.index.drop_off");
      case "Alternate" : return this.locale("offers.index.alternate");
    }
  },

  gogovan_status: Ember.computed("delivery.gogovanOrder.status", function(){
    var ggvStatus = this.get("delivery.gogovanOrder.status");
    switch(ggvStatus) {
      case "pending": return this.locale("offers.index.van_booked");
      case "active": return this.locale("offers.index.van_confirmed");
      case "completed": return this.locale("offers.index.picked_up");
    }
  }),

  isOffer: Ember.computed('this', function(){
    return this.get('constructor.modelName') === 'offer';
  }),

  // unread offer-items messages
  unreadMessagesCount: Ember.computed('messages.@each.state', function(){
    return this.get('messages').filterBy('state', 'unread').length;
  }),

  hasUnreadMessages: Ember.computed('unreadMessagesCount', function(){
    return this.get('unreadMessagesCount') > 0;
  }),

  // unread offer-messages
  unreadOfferMessages: Ember.computed('messages.@each.state', function(){
    return this.get('messages').filterBy('state', 'unread').filterBy('item', null).sortBy('createdAt');
  }),

  unreadOfferMessagesCount: Ember.computed('unreadOfferMessages', function(){
    var count = this.get('unreadOfferMessages.length');
    return count > 0 ? count : '';
  }),

  // unread offer-messages by donor
  hasUnreadDonorMessages: Ember.computed('unreadOfferMessages', function(){
    return this.get('unreadOfferMessages').filterBy('isPrivate', false).length > 0;
  }),

  // unread offer-messages by supervisor-reviewer
  hasUnreadPrivateMessages: Ember.computed('unreadOfferMessages', function(){
    return this.get('unreadOfferMessages').filterBy('isPrivate', true).length > 0;
  }),

  // recent offer message
  lastMessage: Ember.computed('messages.[]', function(){
    var messages = this.get('messages').filterBy('item', null).sortBy('createdAt');
    return messages.get('length') > 0 ? messages.get('lastObject') : null;
  }),

  hasCrossroadsTransport: Ember.computed('crossroadsTransport', function(){
    return this.get('crossroadsTransport') && this.get('crossroadsTransport.isVanAllowed')
  }),

  hasGogovanTransport: Ember.computed('gogovanTransport', function(){
    return this.get('gogovanTransport') && !this.get('gogovanTransport.disabled');
  }),

  // display "General Messages Thread"
  displayGeneralMessages: Ember.computed('isDraft', 'lastMessage', function(){
    return !(this.get('isDraft') && this.get('lastMessage') === null);
  }),

  // to sort on offer-details page for updated-offer and latest-message
  latestUpdatedTime: Ember.computed('lastMessage', function(){
    var value;
    switch(Ember.compare(this.get('lastMessage.createdAt'), this.get('updatedAt'))) {
      case 0 :
      case 1 : value = this.get('lastMessage.createdAt'); break;
      case -1 : value = this.get('updatedAt'); break;
    }
    return value;
  }),

  hasCompleteGGVOrder: Ember.computed('delivery.gogovanOrder.status', function(){
    return (this.get("delivery.gogovanOrder.status") || "") === "completed";
  }),

  showOfferIcons: Ember.computed('hasCompleteGGVOrder','itemCount', 'isClosed', 'hasReceived', function(){
    return this.get("itemCount") > 0 && !(this.get('isClosed') || this.get('hasReceived')) && !this.get("hasCompleteGGVOrder");
  }),

  statusBarClass: Ember.computed('state', function(){
    if(this.get("isSubmitted")){ return "is-submitted"}
    else if(this.get("isUnderReview")){ return "is-under-review"}
    else if(this.get("isReviewed")){return "is-reviewed"}
    else if(this.get("isScheduled")){return "is-scheduled"}
    else if(this.get("isClosed")){return "is-closed"}
    else if(this.get("hasReceived")){return "is-received"}
  }),

  showDeliveryDetails: Ember.computed('state', function(){
    return this.get("isScheduled") || this.get("isReceived") || this.get("isReceiving");
  }),

  hideBookingModification: Ember.computed.alias("delivery.gogovanOrder.isCompleted"),

  allPackagesMissing: Ember.computed("state", "items.@each.state", "packages.@each.state", function(){
    return !this.get("allItemsRejected") &&
      this.get("allItemsReviewed") &&
      this.get("state") !== "received" &&
      this.get("packages.length") > 0 &&
      this.get("packages").filter(p => !p.get("item.isRejected") && p.get("state") === "missing").get("length") === this.get("packages.length");
  }),

  timeDetail: Ember.computed("state", "delivery", function(){
    var prefix = "", suffix = "", date;

    if(this.get("isSubmitted")) {
      prefix = this.locale("submitted");
      date = this.get("submittedAt");

    } else if(this.get("isUnderReview")) {
      prefix = this.get("i18n").t("review_offer.review_started_by",
        { firstName: this.get("reviewedBy.firstName"),
          lastName: this.get("reviewedBy.lastName") }
      );
      date = this.get("reviewedAt");

    } else if(this.get("isReviewed")) {
      prefix = this.locale("review_offer.reviewed");
      date = this.get("reviewCompletedAt");
      suffix = this.locale("review_offer.plan_transport");

    } else if(this.get("isClosed")) {
      prefix = this.get("i18n").t("offer.closed_by",
        { firstName: this.get("closedBy.firstName"),
          lastName: this.get("closedBy.lastName") }
      );
      date = this.get("reviewCompletedAt");

    } else if(this.get("isCancelled")) {
      prefix = this.get("i18n").t("offer.cancelled_by",
        { firstName: this.get("createdBy.firstName"),
          lastName: this.get("createdBy.lastName") }
      );
      date = this.get("cancelledAt");

    } else if(this.get("isReceived")) {
      prefix = this.get("i18n").t("offer.received_by",
        { firstName: this.get("createdBy.firstName"),
          lastName: this.get("createdBy.lastName") }
      );
      date = this.get("receivedAt");

    } else if(this.get("isReceiving")) {
      prefix = this.get("i18n").t("offer.offer_details.start_receiving_by",
        { firstName: this.get("receivedBy.firstName"),
          lastName: this.get("receivedBy.lastName") }
      );
      date = this.get("startReceivingAt");

    } else if(this.get("isScheduled")) {
      if(this.get("delivery.isAlternate")) {
        prefix = this.locale('offer.offer_details.is_collection');
      } else if (this.get("delivery.isDropOff")) {
        prefix = this.locale('offer.offer_details.is_drop_off');
      } else if (this.get("delivery.isGogovan")){
        prefix = this.get("delivery.gogovanOrder.ggvOrderStatus");
      }

      if (this.get("delivery.isGogovan")) {
        if (this.get("delivery.completedWithGogovan")) {
          date = this.get("delivery.gogovanOrder.completedAt");
        } else {
          prefix = prefix + " " + this.get("delivery.schedule.slotName");
          date = this.get("delivery.schedule.scheduledAt");
        }
      } else {
        date = this.get("delivery.schedule.scheduledAt");
        suffix = this.get("delivery.schedule.dayTime");
      }
    }
    return { prefix: prefix, date: date, suffix: suffix };
  }),

  itemStatus: Ember.computed("state", "items.@each.state", "packages.@each.state", function(){
    if (this.get("hasReceived")) {
      return this.get("receivedCount") + " " +
        this.locale("offer.offer_details.received") + ", " +
        this.get("missingCount") + " " +
        this.locale("offer.offer_details.missing") + ", " +
        this.get("rejectedItems.length") + " " +
        this.locale("offer.offer_details.rejected");
    } else {
      return this.get("approvedItems.length") + " " +
        this.locale("offer.offer_details.accepted") + ", " +
        this.get("rejectedItems.length") + " " +
        this.locale("offer.offer_details.rejected") + ", " +
        this.get("submittedItems.length") + " " +
        this.locale("offer.offer_details.pending");
    }
  }),

});
<commit_msg>GCW-965 fixed offer-details of received and cancelled offers<commit_after>import Ember from 'ember';
import DS from 'ember-data';
const { getOwner } = Ember;

var attr = DS.attr,
    hasMany = DS.hasMany,
    belongsTo = DS.belongsTo;

export default DS.Model.extend({
  language:       attr('string'),
  state:          attr('string', {defaultValue: 'draft'}),
  origin:         attr('string'),
  stairs:         attr('boolean'),
  parking:        attr('boolean'),
  estimatedSize:  attr('string'),
  notes:          attr('string'),
  createdAt:      attr('date'),
  updatedAt:      attr('date'),
  submittedAt:    attr('date'),
  cancelledAt:    attr('date'),
  state_event:    attr('string'),
  reviewedAt:     attr('date'),
  receivedAt:     attr('date'),
  reviewCompletedAt: attr('date'),
  deliveredBy:    attr('string'),
  startReceivingAt: attr('date'),
  cancelReason:   attr('string'),

  gogovanTransport:    belongsTo('gogovan_transport', { async: false }),
  crossroadsTransport: belongsTo('crossroads_transport', { async: false }),
  cancellationReason:  belongsTo('cancellation_reason', { async: false }),

  // used for items of current-offer
  saleable:       attr('boolean'),

  items:          hasMany('item', { async: false }),
  messages:       hasMany('message', { async: false }),

  delivery:       belongsTo('delivery', { async: false }),
  createdBy:      belongsTo('user', { async: false }),
  reviewedBy:     belongsTo('user', { async: false }),
  closedBy:       belongsTo('user', { async: false }),
  receivedBy:     belongsTo('user', { async: false }),

  // User details
  userName:       attr('string'),
  userPhone:      attr('string'),

  crossroadsTruckCost: Ember.computed('crossroadsTransport', function(){
    return this.get('crossroadsTransport.cost');
  }),

  itemCount: Ember.computed('items.@each.state', function(){
    return this.get("items").rejectBy("state", "draft").length;
  }),

  allPackages: Ember.computed(function(){
    return this.store.peekAll("package");
  }),

  packages: Ember.computed("allPackages.@each.offerId", function(){
    return this.get("allPackages").filterBy("offerId", parseInt(this.get("id")));
  }),

  itemPackages: Ember.computed.alias("packages"),

  receivedCount: Ember.computed("packages.@each.state", function(){
    return this.get('packages').filterBy("state", "received").length;
  }),

  missingCount: Ember.computed("packages.@each.state", function(){
    return this.get('packages').filterBy("state", "missing").length;
  }),

  approvedItems: Ember.computed.filterBy("items", "state", "accepted"),
  rejectedItems: Ember.computed.filterBy("items", "state", "rejected"),
  submittedItems: Ember.computed.filterBy("items", "state", "submitted"),
  isDraft: Ember.computed.equal("state", "draft"),
  isInactive: Ember.computed.equal("state", "inactive"),
  isSubmitted: Ember.computed.equal("state", "submitted"),
  isScheduled: Ember.computed.equal("state", "scheduled"),
  isUnderReview: Ember.computed.equal("state", "under_review"),
  isReviewed: Ember.computed.equal("state", "reviewed"),
  isClosed: Ember.computed.equal("state", "closed"),
  isReceived: Ember.computed.equal("state", "received"),
  isReceiving: Ember.computed.equal("state", "receiving"),
  isCancelled: Ember.computed.equal("state", "cancelled"),
  preventNewItem: Ember.computed.alias("isFinished"),

  hasReceived: Ember.computed.or('isReceived', 'isReceiving'),
  isReviewing: Ember.computed.or('isUnderReview', 'isReviewed'),
  adminCurrentOffer: Ember.computed.or('isReviewing', 'isScheduled'),
  nonSubmittedOffer: Ember.computed.or('isDraft', 'isInactive'),
  closedOrCancelled: Ember.computed.or('isClosed', 'isCancelled'),

  needReview: Ember.computed('isUnderReview', 'isSubmitted', 'isClosed', function(){
    return this.get('isUnderReview') || this.get('isSubmitted') || this.get("isClosed");
  }),

  isFinished: Ember.computed('isClosed', 'isReceived', 'isCancelled', 'isInactive', function(){
    return this.get('isClosed') || this.get('isReceived') || this.get('isCancelled') || this.get('isInactive');
  }),

  canMerged: Ember.computed('isSubmitted', 'isUnderReview', 'isReviewed', function(){
    return this.get('isSubmitted') || this.get('isUnderReview') || this.get('isReviewed');
  }),

  baseForMerge: Ember.computed('isSubmitted', 'isUnderReview', 'isReviewed', 'isScheduled', function(){
    return this.get('isSubmitted') || this.get('isUnderReview') || this.get('isReviewed') || this.get('isScheduled');
  }),

  activeItems: Ember.computed('items.@each.state', function(){
    return this.get('items').rejectBy("state", "draft");
  }),

  nonEmptyOffer: Ember.computed('items.[]', function(){
    return this.get('itemCount') > 0;
  }),

  allItemsReviewed: Ember.computed('items.@each.state', function(){
    var reviewedItems = this.get('activeItems').filterBy('state', 'submitted');
    return reviewedItems.get('length') === 0;
  }),

  readyForSchedule: Ember.computed('needReview', 'allItemsReviewed', function(){
    return this.get('needReview') && this.get('allItemsReviewed');
  }),

  allItemsRejected: Ember.computed('items.@each.state', 'needReview', function(){
    var rejectedItems = this.get('activeItems').filterBy('state', 'rejected');
    return this.get('needReview') && (rejectedItems.get('length') === this.get('itemCount'));
  }),

  displayImageUrl: Ember.computed('items.@each.displayImageUrl', function(){
    return this.get("activeItems.firstObject.displayImageUrl") || "assets/images/default_item.jpg";
  }),

  isCharitableSale: Ember.computed('items.@each.saleable', function(){
    var isSaleable = this.get("items").rejectBy("saleable", false).length > 0;
    return  isSaleable ? this.locale("yes") : this.locale("no");
  }),

  isAccepted: Ember.computed('items.@each.saleable', function(){
    return (this.get("approvedItems").length > 0) && this.get('isReviewed');
  }),

  status: Ember.computed('state', function(){
    var state = this.get('state');
    var status;
    switch(state) {
      case 'draft': return this.locale('offers.index.complete_offer');
      case 'under_review' : return this.locale('offers.index.in_review');
      case 'submitted' : return this.locale('offers.index.awaiting_review');
      case 'reviewed' : return this.locale('offers.index.arrange_transport');
      case 'scheduled' : return this.scheduledStatus();
      case 'closed' : return this.locale('offers.index.closed');
      case 'received' : return this.locale('offers.index.received');
      case 'receiving' : return this.locale('offers.index.receiving');
      case 'inactive' : return this.locale('offers.index.inactive');
    }
    return status;
  }),

  i18n: Ember.inject.service(),

  locale: function(text) {
    return this.get("i18n").t(text);
  },

  statusText: Ember.computed('status', 'itemCount', function(){
    return this.get("nonSubmittedOffer") ? this.get("status") : (this.get("status") + " ("+ this.get("itemCount") + " "+ this.locale("items_text") +")")
  }),

  scheduledStatus: function(){
    var deliveryType = this.get("delivery.deliveryType")
    switch(deliveryType) {
      case "Gogovan" : return this.get("gogovan_status");
      case "Drop Off" : return this.locale("offers.index.drop_off");
      case "Alternate" : return this.locale("offers.index.alternate");
    }
  },

  gogovan_status: Ember.computed("delivery.gogovanOrder.status", function(){
    var ggvStatus = this.get("delivery.gogovanOrder.status");
    switch(ggvStatus) {
      case "pending": return this.locale("offers.index.van_booked");
      case "active": return this.locale("offers.index.van_confirmed");
      case "completed": return this.locale("offers.index.picked_up");
    }
  }),

  isOffer: Ember.computed('this', function(){
    return this.get('constructor.modelName') === 'offer';
  }),

  // unread offer-items messages
  unreadMessagesCount: Ember.computed('messages.@each.state', function(){
    return this.get('messages').filterBy('state', 'unread').length;
  }),

  hasUnreadMessages: Ember.computed('unreadMessagesCount', function(){
    return this.get('unreadMessagesCount') > 0;
  }),

  // unread offer-messages
  unreadOfferMessages: Ember.computed('messages.@each.state', function(){
    return this.get('messages').filterBy('state', 'unread').filterBy('item', null).sortBy('createdAt');
  }),

  unreadOfferMessagesCount: Ember.computed('unreadOfferMessages', function(){
    var count = this.get('unreadOfferMessages.length');
    return count > 0 ? count : '';
  }),

  // unread offer-messages by donor
  hasUnreadDonorMessages: Ember.computed('unreadOfferMessages', function(){
    return this.get('unreadOfferMessages').filterBy('isPrivate', false).length > 0;
  }),

  // unread offer-messages by supervisor-reviewer
  hasUnreadPrivateMessages: Ember.computed('unreadOfferMessages', function(){
    return this.get('unreadOfferMessages').filterBy('isPrivate', true).length > 0;
  }),

  // recent offer message
  lastMessage: Ember.computed('messages.[]', function(){
    var messages = this.get('messages').filterBy('item', null).sortBy('createdAt');
    return messages.get('length') > 0 ? messages.get('lastObject') : null;
  }),

  hasCrossroadsTransport: Ember.computed('crossroadsTransport', function(){
    return this.get('crossroadsTransport') && this.get('crossroadsTransport.isVanAllowed')
  }),

  hasGogovanTransport: Ember.computed('gogovanTransport', function(){
    return this.get('gogovanTransport') && !this.get('gogovanTransport.disabled');
  }),

  // display "General Messages Thread"
  displayGeneralMessages: Ember.computed('isDraft', 'lastMessage', function(){
    return !(this.get('isDraft') && this.get('lastMessage') === null);
  }),

  // to sort on offer-details page for updated-offer and latest-message
  latestUpdatedTime: Ember.computed('lastMessage', function(){
    var value;
    switch(Ember.compare(this.get('lastMessage.createdAt'), this.get('updatedAt'))) {
      case 0 :
      case 1 : value = this.get('lastMessage.createdAt'); break;
      case -1 : value = this.get('updatedAt'); break;
    }
    return value;
  }),

  hasCompleteGGVOrder: Ember.computed('delivery.gogovanOrder.status', function(){
    return (this.get("delivery.gogovanOrder.status") || "") === "completed";
  }),

  showOfferIcons: Ember.computed('hasCompleteGGVOrder','itemCount', 'isClosed', 'hasReceived', function(){
    return this.get("itemCount") > 0 && !(this.get('isClosed') || this.get('hasReceived')) && !this.get("hasCompleteGGVOrder");
  }),

  statusBarClass: Ember.computed('state', function(){
    if(this.get("isSubmitted")){ return "is-submitted"}
    else if(this.get("isUnderReview")){ return "is-under-review"}
    else if(this.get("isReviewed")){return "is-reviewed"}
    else if(this.get("isScheduled")){return "is-scheduled"}
    else if(this.get("isClosed")){return "is-closed"}
    else if(this.get("hasReceived")){return "is-received"}
  }),

  showDeliveryDetails: Ember.computed('state', function(){
    return this.get("isScheduled") || this.get("isReceived") || this.get("isReceiving");
  }),

  hideBookingModification: Ember.computed.alias("delivery.gogovanOrder.isCompleted"),

  allPackagesMissing: Ember.computed("state", "items.@each.state", "packages.@each.state", function(){
    return !this.get("allItemsRejected") &&
      this.get("allItemsReviewed") &&
      this.get("state") !== "received" &&
      this.get("packages.length") > 0 &&
      this.get("packages").filter(p => !p.get("item.isRejected") && p.get("state") === "missing").get("length") === this.get("packages.length");
  }),

  timeDetail: Ember.computed("state", "delivery", function(){
    var prefix = "", suffix = "", date;

    if(this.get("isSubmitted")) {
      prefix = this.locale("submitted");
      date = this.get("submittedAt");

    } else if(this.get("isUnderReview")) {
      prefix = this.get("i18n").t("review_offer.review_started_by",
        { firstName: this.get("reviewedBy.firstName"),
          lastName: this.get("reviewedBy.lastName") }
      );
      date = this.get("reviewedAt");

    } else if(this.get("isReviewed")) {
      prefix = this.locale("review_offer.reviewed");
      date = this.get("reviewCompletedAt");
      suffix = this.locale("review_offer.plan_transport");

    } else if(this.get("isClosed")) {
      prefix = this.get("i18n").t("offer.closed_by",
        { firstName: this.get("closedBy.firstName"),
          lastName: this.get("closedBy.lastName") }
      );
      date = this.get("reviewCompletedAt");

    } else if(this.get("isCancelled")) {
      prefix = this.get("i18n").t("offer.cancelled_by",
        { firstName: this.get("closedBy.firstName"),
          lastName: this.get("closedBy.lastName") }
      );
      date = this.get("cancelledAt");

    } else if(this.get("isReceived")) {
      prefix = this.get("i18n").t("offer.received_by",
        { firstName: this.get("closedBy.firstName"),
          lastName: this.get("closedBy.lastName") }
      );
      date = this.get("receivedAt");

    } else if(this.get("isReceiving")) {
      prefix = this.get("i18n").t("offer.offer_details.start_receiving_by",
        { firstName: this.get("receivedBy.firstName"),
          lastName: this.get("receivedBy.lastName") }
      );
      date = this.get("startReceivingAt");

    } else if(this.get("isScheduled")) {
      if(this.get("delivery.isAlternate")) {
        prefix = this.locale('offer.offer_details.is_collection');
      } else if (this.get("delivery.isDropOff")) {
        prefix = this.locale('offer.offer_details.is_drop_off');
      } else if (this.get("delivery.isGogovan")){
        prefix = this.get("delivery.gogovanOrder.ggvOrderStatus");
      }

      if (this.get("delivery.isGogovan")) {
        if (this.get("delivery.completedWithGogovan")) {
          date = this.get("delivery.gogovanOrder.completedAt");
        } else {
          prefix = prefix + " " + this.get("delivery.schedule.slotName");
          date = this.get("delivery.schedule.scheduledAt");
        }
      } else {
        date = this.get("delivery.schedule.scheduledAt");
        suffix = this.get("delivery.schedule.dayTime");
      }
    }
    return { prefix: prefix, date: date, suffix: suffix };
  }),

  itemStatus: Ember.computed("state", "items.@each.state", "packages.@each.state", function(){
    if (this.get("hasReceived")) {
      return this.get("receivedCount") + " " +
        this.locale("offer.offer_details.received") + ", " +
        this.get("missingCount") + " " +
        this.locale("offer.offer_details.missing") + ", " +
        this.get("rejectedItems.length") + " " +
        this.locale("offer.offer_details.rejected");
    } else {
      return this.get("approvedItems.length") + " " +
        this.locale("offer.offer_details.accepted") + ", " +
        this.get("rejectedItems.length") + " " +
        this.locale("offer.offer_details.rejected") + ", " +
        this.get("submittedItems.length") + " " +
        this.locale("offer.offer_details.pending");
    }
  }),

});
<|endoftext|>


<commit_before>const td              = require('testdouble');
const Promise         = require('rsvp').Promise;
const expect          = require('../../../../helpers/expect');

describe('iOS Boot Emulator Task', function() {
  afterEach(function() {
    td.reset();
  });

  it('spawns xcrun', function() {
    let spawnDouble = td.replace('../../../../../lib/utils/spawn');
    let bootEm = require('../../../../../lib/targets/ios/tasks/boot-emulator');

    bootEm({uuid: 'id',});
    td.verify(spawnDouble(
      '/usr/bin/xcrun',
      ['simctl', 'boot', 'id']
    ));
  });

  it('does not spawn boot if the emulator state is Booted', function() {
    let invokes = [];

    td.replace('../../../../../lib/utils/spawn', function(cmd, args) {
      invokes.push([...arguments]);
      return Promise.resolve();
    });

    let bootEm = require('../../../../../lib/targets/ios/tasks/boot-emulator');

    return bootEm({id: 'emulatorId', state: 'Booted'}, 'appName', 'builtPath').then(function() {
      expect(invokes.length).to.equal(0);
    });
  });
});
<commit_msg>refactor(ios-boot-emulator-test): update tests<commit_after>const td        = require('testdouble');
const Promise   = require('rsvp').Promise;
const expect    = require('../../../../helpers/expect');

const uuid      = 'uuid';
const spawnArgs = ['/usr/bin/xcrun', ['simctl', 'boot', uuid]];

describe('iOS Boot Emulator Task', () => {
  let bootEmulator;
  let spawn;

  beforeEach(() => {
    spawn = td.replace('../../../../../lib/utils/spawn');
    td.when(spawn(...spawnArgs)).thenReturn(Promise.resolve());

    bootEmulator = require('../../../../../lib/targets/ios/tasks/boot-emulator');
  });

  afterEach(() => {
    td.reset();
  });

  it('does not spawn boot if the emulator state is Booted', () => {
    return bootEmulator({ uuid, state: 'Booted' }).then(() => {
      td.verify(spawn(), { ignoreExtraArgs: true, times: 0 });
    });
  });

  it('calls spawn with correct arguments', () => {
    td.config({ ignoreWarnings: true });

    td.when(spawn(), { ignoreExtraArgs: true })
      .thenReturn(Promise.resolve());

    return bootEmulator({ uuid }).then(() => {
      td.verify(spawn(...spawnArgs));

      td.config({ ignoreWarnings: false });
    });
  });

  it('spawns xcrun and resolves on exit', () => {
    expect(bootEmulator({ uuid })).to.eventually.be.fulfilled;
  });

  it('bubbles up error message when spawn rejects', () => {
    td.when(spawn(...spawnArgs)).thenReturn(Promise.reject('spawn error'));

    return expect(bootEmulator({ uuid }))
      .to.eventually.be.rejectedWith('spawn error');
  });
});
<|endoftext|>


<commit_before>/**
 * track model
 * - maintained by post model: updateByEid()
 * - read by hot tracks controller: fetchPosts()
 * - read by notif template: fetchPosts()
 * @author adrienjoly, whyd
 **/

/*
  - - - structure of a track record - - -
  _id: automatically-assigned identifier (ObjectId)
  eId: uri used to identify and embed this track's stream from its source
  img: url of the image thumb of that track
  name: name of the track (combines artist and title) // TODO: which version is stored in case of conflict?
  score: the higher this number, the higher this track will rank in the hot tracks. computed from various metrics.
  nbR: number of posts/reposts within the HOT_TRACK_TIME_WINDOW period
  nbL: number of likes
  nbP: number of plays
  prev: score value at last snapshot (used to compute trends)
  meta: {art, tit, dur, c, t}: concise metadata extracted from 3rd-parties (artist name, track title, duration, confidence 0<1, timestamp)
  alt: [eId1, eId2...]: array of alternative souces for that track (cross-platform mappings)

  - - - old fields - - -
  prevRank: used to store the rank/index of that record in the track collection, before the score was updated
  nbFreshPosts: used to store the number of posts within the HOT_TRACK_TIME_WINDOW
  nbTotalPosts: duplicate of nbR
  nbTotalLikes: duplicate of nbL
  nbTotalPlays: duplicate of nbP

  - - - notice - - -
  since the periodic call of refreshHotTracksCache() was removed:
  -> the prevRank is never updated => no movement information will be displayed on openwhyd.org/hot
  -> the score of a track will only be updated on a (re-)post, like, or play operation on that track
*/

var mongodb = require('./mongodb.js');
var ObjectId = mongodb.ObjectId;
var snip = require('../snip.js');
var metadataResolver = require('../models/metadataResolver.js');

var FIELDS_TO_SUM = {
  nbP: true, // number of plays
  nbL: true, // number of likes (from lov[] field)
  nbR: true, // number of posts/reposts
};

var FIELDS_TO_COPY = {
  name: true,
  img: true,
  score: true,
};

var COEF_REPOST = 100;
var COEF_LIKE = 50;
var COEF_PLAY = 1;

var HOT_TRACK_TIME_WINDOW = 7 * 24 * 60 * 60 * 1000; // count (re)posts that are less than 1 week old, for ranking

function scorePost(post) {
  return COEF_REPOST * post.nbR + COEF_LIKE * post.nbL + COEF_PLAY * post.nbP;
}

var POST_FETCH_OPTIONS = {
  limit: 10000,
  sort: [['_id', 'desc']],
};

// core methods

function save(track, cb, replace) {
  var op = replace ? track : { $set: track };
  mongodb.collections['track'].updateOne(
    { eId: track.eId },
    op, // TODO: always use $set operator, to prevent "Update document requires atomic operators" ? (see https://github.com/openwhyd/openwhyd/issues/441#issuecomment-774697717)
    { upsert: true },
    function (error, result) {
      //console.log("=> saved hot track:", result);
      if (error) console.error('track.save() db error:', error);
      if (cb) cb(result);
    }
  );
}

function remove(q, cb) {
  mongodb.collections['track'].deleteOne(q, function (error, result) {
    console.log('=> removed hot track:', q);
    if (error) console.error('track.remove() error: ' + error.stack);
    if (cb) cb(result);
  });
}

exports.countTracksWithField = function (fieldName, cb) {
  var q = {};
  q[fieldName] = { $exists: 1 };
  mongodb.collections['track'].countDocuments(q, cb);
};

/* fetch top hot tracks, without processing */
exports.fetch = function (params, handler) {
  params = params || {};
  params.sort = params.sort || [['score', 'desc']];
  mongodb.collections['track'].find({}, params, function (err, cursor) {
    cursor.toArray(function (err, results) {
      // console.log('=> fetched ' + results.length + ' tracks');
      if (handler) handler(results);
    });
  });
};

exports.fetchTrackByEid = function (eId, cb) {
  // in order to allow requests of soundcloud eId without hash (#):
  var eidPrefix = ('' + eId).indexOf('/sc/') == 0 && ('' + eId).split('#')[0];
  mongodb.collections['track'].findOne({ eId: eId }, function (err, track) {
    if (!err && !track && eidPrefix)
      mongodb.collections['track'].findOne(
        { eId: new RegExp('^' + eidPrefix + '.*') },
        function (err, track) {
          if (track && track.eId.split('#')[0] != eidPrefix) track = null;
          cb(err ? { error: err } : track);
        }
      );
    else cb(err ? { error: err } : track);
  });
};

// functions for fetching tracks and corresponding posts

var fieldList = Object.keys(FIELDS_TO_COPY)
  .concat(Object.keys(FIELDS_TO_SUM))
  .concat(['prev']);

function mergePostData(track, post) {
  for (let f in fieldList) post[fieldList[f]] = track[fieldList[f]];
  post.trackId = track._id;
  post.rankIncr = track.prev - track.score;
  return post;
}

function fetchPostsByPid(pId, cb) {
  var pidList = (pId && Array.isArray(pId) ? pId : []).map(function (id) {
    return ObjectId('' + id);
  });
  //for (let i in pidList) pidList[i] = ObjectId("" + pidList[i]);
  mongodb.collections['post'].find(
    { _id: { $in: pidList } },
    POST_FETCH_OPTIONS,
    function (err, cursor) {
      cursor.toArray(function (err, posts) {
        cb(posts);
      });
    }
  );
}

/* fetch top hot tracks, and include complete post data (from the "post" collection), score, and rank increment */
exports.fetchPosts = function (params, handler) {
  params = params || {};
  var firstIndex = (params.skip = parseInt(params.skip || 0));
  exports.fetch(params, function (tracks) {
    var pidList = snip.objArrayToValueArray(tracks, 'pId');
    fetchPostsByPid(pidList, function (posts) {
      var postsByEid = snip.objArrayToSet(posts, 'eId');
      for (let i in tracks) {
        var track = tracks[i];
        if (!track) {
          console.error('warning: skipping null track in track.fetchPosts()');
          continue;
        }
        var post = postsByEid[tracks[i].eId];
        if (!post) {
          //console.error("warning: skipping null post in track.fetchPosts()");
          continue;
        }
        tracks[i] = mergePostData(track, post, firstIndex + parseInt(i));
      }
      handler(tracks, { postsByEid: postsByEid });
    });
  });
};

// update function

function fetchPostsByEid(eId, cb) {
  var criteria = { eId: eId && Array.isArray(eId) ? { $in: eId } : eId };
  mongodb.collections['post'].find(
    criteria,
    POST_FETCH_OPTIONS,
    function (err, cursor) {
      cursor.toArray(function (err, posts) {
        cb(posts);
      });
    }
  );
}

// called when a track is updated/deleted by a user
exports.updateByEid = function (eId, cb, replace, additionalFields) {
  var since = Date.now() - HOT_TRACK_TIME_WINDOW;
  console.log('track.updateByEid: ', eId);
  fetchPostsByEid(eId, function (posts) {
    if (!posts || !posts.length) return remove({ eId: eId }, cb);
    // 0) init track objects (one for storage and display, one for scoring over the selected period of time)
    var freshTrackStats = {},
      track = {
        eId: eId,
        nbR: posts.length,
      };
    for (let f in FIELDS_TO_SUM) {
      track[f] = track[f] || 0;
      freshTrackStats[f] = freshTrackStats[f] || 0;
    }
    // 1) score posts, to select which user will be featured for this track
    for (let p in posts) {
      posts[p].nbL = (posts[p].lov || []).length;
      for (let f in FIELDS_TO_SUM) posts[p][f] = posts[p][f] || 0;
      posts[p].score = scorePost(posts[p]);
      track.nbP += posts[p].nbP;
      track.nbL += posts[p].nbL;
      if (
        posts[p]._id.getTimestamp().getTime() > since &&
        posts[p].ctx != 'hot'
      ) {
        ++freshTrackStats.nbR;
      }
    }
    posts.sort(function (a, b) {
      return b.score - a.score;
    });
    // 2) populate and save track object, based on best-scored post
    track.pId = posts[0]._id;
    for (let f in FIELDS_TO_COPY) track[f] = posts[0][f];
    track.score = scorePost(freshTrackStats);
    if (additionalFields) {
      console.log(
        'storing additional fields',
        Object.keys(additionalFields),
        '...'
      );
      for (let f in additionalFields) track[f] = additionalFields[f];
    }
    console.log('saving track', track);
    save(track, cb, replace);
  });
};

var CONCISE_META_FIELDS = {
  trackTitle: 'tit',
  artistName: 'art',
  duration: 'dur',
  confidence: 'c',
};

var MINIMUM_CONFIDENCE = 0.8;

function translateTrackToConciseMetadata(track = {}) {
  var concise = {};
  var meta = track.metadata || {};
  var trackId = meta.uri || meta.id;
  if (
    /*meta.trackTitle && meta.artistName &&*/ meta.confidence >=
    MINIMUM_CONFIDENCE
  ) {
    concise.meta = snip.filterFields(meta, CONCISE_META_FIELDS);
  }
  if (typeof track.mappings == 'object') {
    concise.alt = [];
    for (let sourceId in track.mappings) {
      var alt = track.mappings[sourceId];
      if (alt.id != trackId && alt.c >= MINIMUM_CONFIDENCE)
        concise.alt.push('/' + sourceId + '/' + (alt.uri || alt.id));
    }
  }
  return concise;
}

// translates resulting metadata and mappings from metadataResolver,
// to whyd's track collection format (concise fields)
exports.fetchConciseMetadataForEid = function (eId, cb) {
  metadataResolver.fetchMetadataForEid(eId, function (err, track) {
    //console.log("fetchMetadataForEid =>", track);
    cb(err ? { error: '' + err } : translateTrackToConciseMetadata(track));
  });
};

// maintenance functions

exports.snapshotTrackScores = function (cb) {
  mongodb.collections['track'].countDocuments(function (err, count) {
    var i = 0;
    mongodb.forEach2(
      'track',
      { fields: { score: 1 } },
      function (track, next, closeCursor) {
        if (!track || track.error) {
          cb();
          closeCursor();
        } else {
          if (count < 1000) {
            console.log(`snapshotTrackScores ${i + 1} / ${count}`);
          } else if (count % 1000 === 0) {
            console.log(
              `snapshotTrackScores ${i / 1000}k / ${Math.floor(count / 1000)}k`
            );
          }
          ++i;
          mongodb.collections['track'].updateOne(
            { _id: track._id },
            { $set: { prev: track.score } },
            next
          );
        }
      }
    );
  });
};

exports.refreshTrackCollection = function (cb) {
  mongodb.collections['track'].countDocuments(function (err, count) {
    var i = 0;
    mongodb.forEach2(
      'track',
      { fields: { _id: 0, eId: 1 } },
      function (track, next, closeCursor) {
        if (!track || track.error) {
          cb();
          closeCursor();
        } else {
          console.log('refreshHotTracksCache', ++i, '/', count);
          exports.updateByEid(track.eId, next, true);
        }
      }
    );
  });
};

exports.model = exports;
<commit_msg>refactor(hot-tracks): Simplification & clean-up (#536)<commit_after>/**
 * track model
 * - maintained by post model: updateByEid()
 * - read by hot tracks controller: fetchPosts()
 * - read by notif template: fetchPosts()
 * @author adrienjoly, whyd
 **/

/*
  - - - structure of a track record - - -
  _id: automatically-assigned identifier (ObjectId)
  eId: uri used to identify and embed this track's stream from its source
  img: url of the image thumb of that track
  name: name of the track (combines artist and title) // TODO: which version is stored in case of conflict?
  score: the higher this number, the higher this track will rank in the hot tracks. computed from various metrics.
  nbR: number of posts/reposts within the HOT_TRACK_TIME_WINDOW period
  nbL: number of likes
  nbP: number of plays
  prev: score value at last snapshot (used to compute trends)
  meta: {art, tit, dur, c, t}: concise metadata extracted from 3rd-parties (artist name, track title, duration, confidence 0<1, timestamp)
  alt: [eId1, eId2...]: array of alternative souces for that track (cross-platform mappings)

  - - - old fields - - -
  prevRank: used to store the rank/index of that record in the track collection, before the score was updated
  nbFreshPosts: used to store the number of posts within the HOT_TRACK_TIME_WINDOW
  nbTotalPosts: duplicate of nbR
  nbTotalLikes: duplicate of nbL
  nbTotalPlays: duplicate of nbP

  - - - notice - - -
  since the periodic call of refreshHotTracksCache() was removed:
  -> the prevRank is never updated => no movement information will be displayed on openwhyd.org/hot
  -> the score of a track will only be updated on a (re-)post, like, or play operation on that track
*/

var mongodb = require('./mongodb.js');
var ObjectId = mongodb.ObjectId;
var snip = require('../snip.js');
var metadataResolver = require('../models/metadataResolver.js');

var FIELDS_TO_SUM = {
  nbP: true, // number of plays
  nbL: true, // number of likes (from lov[] field)
  nbR: true, // number of posts/reposts
};

var FIELDS_TO_COPY = {
  name: true,
  img: true,
  score: true,
};

var COEF_REPOST = 100;
var COEF_LIKE = 50;
var COEF_PLAY = 1;

var HOT_TRACK_TIME_WINDOW = 7 * 24 * 60 * 60 * 1000; // count (re)posts that are less than 1 week old, for ranking

function scorePost(post) {
  return COEF_REPOST * post.nbR + COEF_LIKE * post.nbL + COEF_PLAY * post.nbP;
}

var POST_FETCH_OPTIONS = {
  limit: 10000,
  sort: [['_id', 'desc']],
};

// core methods

function save(track, cb, replace) {
  var op = replace ? track : { $set: track };
  mongodb.collections['track'].updateOne(
    { eId: track.eId },
    op, // TODO: always use $set operator, to prevent "Update document requires atomic operators" ? (see https://github.com/openwhyd/openwhyd/issues/441#issuecomment-774697717)
    { upsert: true },
    function (error, result) {
      //console.log("=> saved hot track:", result);
      if (error) console.error('track.save() db error:', error);
      if (cb) cb(result);
    }
  );
}

function remove(q, cb) {
  mongodb.collections['track'].deleteOne(q, function (error, result) {
    console.log('=> removed hot track:', q);
    if (error) console.error('track.remove() error: ' + error.stack);
    if (cb) cb(result);
  });
}

exports.countTracksWithField = function (fieldName, cb) {
  var q = {};
  q[fieldName] = { $exists: 1 };
  mongodb.collections['track'].countDocuments(q, cb);
};

/* fetch top hot tracks, without processing */
exports.fetch = function (params, handler) {
  params = params || {};
  params.sort = params.sort || [['score', 'desc']];
  mongodb.collections['track'].find({}, params, function (err, cursor) {
    cursor.toArray(function (err, results) {
      // console.log('=> fetched ' + results.length + ' tracks');
      if (handler) handler(results);
    });
  });
};

exports.fetchTrackByEid = function (eId, cb) {
  // in order to allow requests of soundcloud eId without hash (#):
  var eidPrefix = ('' + eId).indexOf('/sc/') == 0 && ('' + eId).split('#')[0];
  mongodb.collections['track'].findOne({ eId: eId }, function (err, track) {
    if (!err && !track && eidPrefix)
      mongodb.collections['track'].findOne(
        { eId: new RegExp('^' + eidPrefix + '.*') },
        function (err, track) {
          if (track && track.eId.split('#')[0] != eidPrefix) track = null;
          cb(err ? { error: err } : track);
        }
      );
    else cb(err ? { error: err } : track);
  });
};

// functions for fetching tracks and corresponding posts

var fieldList = Object.keys(FIELDS_TO_COPY)
  .concat(Object.keys(FIELDS_TO_SUM))
  .concat(['prev']);

function mergePostData(track, post) {
  for (let f in fieldList) post[fieldList[f]] = track[fieldList[f]];
  post.trackId = track._id;
  post.rankIncr = track.prev - track.score;
  return post;
}

function fetchPostsByPid(pId, cb) {
  var pidList = (pId && Array.isArray(pId) ? pId : []).map(function (id) {
    return ObjectId('' + id);
  });
  //for (let i in pidList) pidList[i] = ObjectId("" + pidList[i]);
  mongodb.collections['post'].find(
    { _id: { $in: pidList } },
    POST_FETCH_OPTIONS,
    function (err, cursor) {
      cursor.toArray(function (err, posts) {
        cb(posts);
      });
    }
  );
}

/* fetch top hot tracks, and include complete post data (from the "post" collection), score, and rank increment */
exports.fetchPosts = function (params, handler) {
  params = params || {};
  params.skip = parseInt(params.skip || 0);
  exports.fetch(params, function (tracks) {
    var pidList = snip.objArrayToValueArray(tracks, 'pId');
    fetchPostsByPid(pidList, function (posts) {
      var postsByEid = snip.objArrayToSet(posts, 'eId');
      for (let i in tracks) {
        var track = tracks[i];
        if (!track) {
          console.error('warning: skipping null track in track.fetchPosts()');
          continue;
        }
        var post = postsByEid[tracks[i].eId];
        if (!post) {
          //console.error("warning: skipping null post in track.fetchPosts()");
          continue;
        }
        tracks[i] = mergePostData(track, post);
      }
      handler(tracks);
    });
  });
};

// update function

function fetchPostsByEid(eId, cb) {
  var criteria = { eId: eId && Array.isArray(eId) ? { $in: eId } : eId };
  mongodb.collections['post'].find(
    criteria,
    POST_FETCH_OPTIONS,
    function (err, cursor) {
      cursor.toArray(function (err, posts) {
        cb(posts);
      });
    }
  );
}

// called when a track is updated/deleted by a user
exports.updateByEid = function (eId, cb, replace, additionalFields) {
  var since = Date.now() - HOT_TRACK_TIME_WINDOW;
  console.log('track.updateByEid: ', eId);
  fetchPostsByEid(eId, function (posts) {
    if (!posts || !posts.length) return remove({ eId: eId }, cb);
    // 0) init track objects (one for storage and display, one for scoring over the selected period of time)
    var freshTrackStats = {},
      track = {
        eId: eId,
        nbR: posts.length,
      };
    for (let f in FIELDS_TO_SUM) {
      track[f] = track[f] || 0;
      freshTrackStats[f] = freshTrackStats[f] || 0;
    }
    // 1) score posts, to select which user will be featured for this track
    for (let p in posts) {
      posts[p].nbL = (posts[p].lov || []).length;
      for (let f in FIELDS_TO_SUM) posts[p][f] = posts[p][f] || 0;
      posts[p].score = scorePost(posts[p]);
      track.nbP += posts[p].nbP;
      track.nbL += posts[p].nbL;
      if (
        posts[p]._id.getTimestamp().getTime() > since &&
        posts[p].ctx != 'hot'
      ) {
        ++freshTrackStats.nbR;
      }
    }
    posts.sort(function (a, b) {
      return b.score - a.score;
    });
    // 2) populate and save track object, based on best-scored post
    track.pId = posts[0]._id;
    for (let f in FIELDS_TO_COPY) track[f] = posts[0][f];
    track.score = scorePost(freshTrackStats);
    if (additionalFields) {
      console.log(
        'storing additional fields',
        Object.keys(additionalFields),
        '...'
      );
      for (let f in additionalFields) track[f] = additionalFields[f];
    }
    console.log('saving track', track);
    save(track, cb, replace);
  });
};

var CONCISE_META_FIELDS = {
  trackTitle: 'tit',
  artistName: 'art',
  duration: 'dur',
  confidence: 'c',
};

var MINIMUM_CONFIDENCE = 0.8;

function translateTrackToConciseMetadata(track = {}) {
  var concise = {};
  var meta = track.metadata || {};
  var trackId = meta.uri || meta.id;
  if (
    /*meta.trackTitle && meta.artistName &&*/ meta.confidence >=
    MINIMUM_CONFIDENCE
  ) {
    concise.meta = snip.filterFields(meta, CONCISE_META_FIELDS);
  }
  if (typeof track.mappings == 'object') {
    concise.alt = [];
    for (let sourceId in track.mappings) {
      var alt = track.mappings[sourceId];
      if (alt.id != trackId && alt.c >= MINIMUM_CONFIDENCE)
        concise.alt.push('/' + sourceId + '/' + (alt.uri || alt.id));
    }
  }
  return concise;
}

// translates resulting metadata and mappings from metadataResolver,
// to whyd's track collection format (concise fields)
exports.fetchConciseMetadataForEid = function (eId, cb) {
  metadataResolver.fetchMetadataForEid(eId, function (err, track) {
    //console.log("fetchMetadataForEid =>", track);
    cb(err ? { error: '' + err } : translateTrackToConciseMetadata(track));
  });
};

// maintenance functions

exports.snapshotTrackScores = function (cb) {
  mongodb.collections['track'].countDocuments(function (err, count) {
    var i = 0;
    mongodb.forEach2(
      'track',
      { fields: { score: 1 } },
      function (track, next, closeCursor) {
        if (!track || track.error) {
          cb();
          closeCursor();
        } else {
          if (count < 1000) {
            console.log(`snapshotTrackScores ${i + 1} / ${count}`);
          } else if (count % 1000 === 0) {
            console.log(
              `snapshotTrackScores ${i / 1000}k / ${Math.floor(count / 1000)}k`
            );
          }
          ++i;
          mongodb.collections['track'].updateOne(
            { _id: track._id },
            { $set: { prev: track.score } },
            next
          );
        }
      }
    );
  });
};

exports.refreshTrackCollection = function (cb) {
  mongodb.collections['track'].countDocuments(function (err, count) {
    var i = 0;
    mongodb.forEach2(
      'track',
      { fields: { _id: 0, eId: 1 } },
      function (track, next, closeCursor) {
        if (!track || track.error) {
          cb();
          closeCursor();
        } else {
          console.log('refreshHotTracksCache', ++i, '/', count);
          exports.updateByEid(track.eId, next, true);
        }
      }
    );
  });
};

exports.model = exports;
<|endoftext|>


<commit_before>
var getTime = require('../medtronic-clock');

function bgTargetsLookup (inputs) {
  return bound_target_range(lookup(inputs));
}

function lookup (inputs) {
    var bgtargets_data = inputs.targets;
    var now = new Date();

    //bgtargets_data.targets.sort(function (a, b) { return a.offset > b.offset });

    var bgTargets = bgtargets_data.targets[bgtargets_data.targets.length - 1]

    for (var i = 0; i < bgtargets_data.targets.length - 1; i++) {
        if ((now >= getTime(bgtargets_data.targets[i].offset)) && (now < getTime(bgtargets_data.targets[i + 1].offset))) {
            bgTargets = bgtargets_data.targets[i];
            break;
        }
    }

    return bgTargets;
}

function bound_target_range (target) {
    // hard-code lower bounds for min_bg and max_bg in case pump is set too low, or units are wrong
    target.max_bg = Math.max(100, target.high);
    target.min_bg = Math.max(90, target.low);
    // hard-code upper bound for min_bg in case pump is set too high
    target.min_bg = Math.min(200, target.min_bg);
    return target
}

bgTargetsLookup.bgTargetsLookup = bgTargetsLookup;
bgTargetsLookup.lookup = lookup;
bgTargetsLookup.bound_target_range = bound_target_range;
exports = module.exports = bgTargetsLookup;

<commit_msg>Lower max override from 100 to 90<commit_after>
var getTime = require('../medtronic-clock');

function bgTargetsLookup (inputs) {
  return bound_target_range(lookup(inputs));
}

function lookup (inputs) {
    var bgtargets_data = inputs.targets;
    var now = new Date();

    //bgtargets_data.targets.sort(function (a, b) { return a.offset > b.offset });

    var bgTargets = bgtargets_data.targets[bgtargets_data.targets.length - 1]

    for (var i = 0; i < bgtargets_data.targets.length - 1; i++) {
        if ((now >= getTime(bgtargets_data.targets[i].offset)) && (now < getTime(bgtargets_data.targets[i + 1].offset))) {
            bgTargets = bgtargets_data.targets[i];
            break;
        }
    }

    return bgTargets;
}

function bound_target_range (target) {
    // hard-code lower bounds for min_bg and max_bg in case pump is set too low, or units are wrong
    target.max_bg = Math.max(90, target.high);
    target.min_bg = Math.max(90, target.low);
    // hard-code upper bound for min_bg in case pump is set too high
    target.min_bg = Math.min(200, target.min_bg);
    return target
}

bgTargetsLookup.bgTargetsLookup = bgTargetsLookup;
bgTargetsLookup.lookup = lookup;
bgTargetsLookup.bound_target_range = bound_target_range;
exports = module.exports = bgTargetsLookup;

<|endoftext|>