<commit_before>"use strict";
let engine = require("engine.io"),
	core = require("../../core"),
	bus = core.bus,
	config = core.config,
	constants = require("../../lib/constants"),
	uid = require("../../lib/uid-server"),
	notify = require("./dispatch"),
	sockets = {},
	packer = require("stringPack")([
		require("../../models/Item"),
		require("../../models/Room"),
		require("../../models/Thread"),
		require("../../models/Text"),
		require("../../models/Note"),
		require("../../models/User"),
		require("../../models/Relation")
	]);

function sendError(socket, code, reason, event) {
	socket.send(JSON.stringify({
		type: "error",
		code,
		reason,
		event: event
	}));
}

bus.on("http/init", app => {
	let socketServer = engine.attach(app.httpServer);

	socketServer.on("connection", socket => {
		let resourceId = uid(16);
		sockets[resourceId] = socket;

		console.log("socket connection created");
		socket.on("close", () => {
			bus.emit("presence/offline", {
				resourceId
			});
			delete sockets[resourceId];
		});

		socket.on("message", message => {
			try {
				message = JSON.parse(message);
			} catch (e) {
				return sendError(socket, "ERR_EVT_PARSE", e.message);
			}

			message.id = uid(16);
			(message.auth = message.auth | {}).resource = resourceId;
			console.log("emitting setstate", message);
			bus.emit("setstate", message, err => {
				console.log(err, message);
				if (err) return sendError(
					socket, err.code || "ERR_UNKNOWN", err.message, message
				);

				if (message.response) {
					if (message.auth && message.auth.session && message.app && message.app.user) {
						bus.emit("presence/online", {
							resource: resourceId,
							user: message.app.user
						});
					}
					socket.send(packer.encode(message.response));
				}
			});
		});
	});
});

bus.on("statechange", changes => {
	notify(changes, core, {}).on("data", (change, rel) => {
		Object.keys(rel.resources).forEach(function (e) {
			if (!sockets[e]) return;
			sockets[e].send(packer.encode(change));
		});
	});
});<commit_msg>Looking for auth.<commit_after>"use strict";
let engine = require("engine.io"),
	core = require("../../core"),
	bus = core.bus,
	config = core.config,
	constants = require("../../lib/constants"),
	uid = require("../../lib/uid-server"),
	notify = require("./dispatch"),
	sockets = {},
	packer = require("stringPack")([
		require("../../models/Item"),
		require("../../models/Room"),
		require("../../models/Thread"),
		require("../../models/Text"),
		require("../../models/Note"),
		require("../../models/User"),
		require("../../models/Relation")
	]);

function sendError(socket, code, reason, event) {
	socket.send(JSON.stringify({
		type: "error",
		code,
		reason,
		event: event
	}));
}

bus.on("http/init", app => {
	let socketServer = engine.attach(app.httpServer);

	socketServer.on("connection", socket => {
		let resourceId = uid(16);
		sockets[resourceId] = socket;

		console.log("socket connection created");
		socket.on("close", () => {
			bus.emit("presence/offline", {
				resourceId
			});
			delete sockets[resourceId];
		});

		socket.on("message", message => {
			try {
				message = JSON.parse(message);
			} catch (e) {
				return sendError(socket, "ERR_EVT_PARSE", e.message);
			}

			message.id = uid(16);
			(message.auth = message.auth | {}).resource = resourceId;
			console.log("emitting setstate", message);
			bus.emit("setstate", message, err => {
				console.log(err, message);
				if (err) return sendError(
					socket, err.code || "ERR_UNKNOWN", err.message, message
				);

				if (message.response) {
					if (message.auth && message.auth.user) {
						bus.emit("presence/online", {
							resource: resourceId,
							user: message.auth.user
						});
					}
					socket.send(packer.encode(message.response));
				}
			});
		});
	});
});

bus.on("statechange", changes => {
	notify(changes, core, {}).on("data", (change, rel) => {
		Object.keys(rel.resources).forEach(function (e) {
			if (!sockets[e]) return;
			sockets[e].send(packer.encode(change));
		});
	});
});<|endoftext|>


<commit_before>/**
 * Module dependencies.
 */

var Emitter = require('emitter');
var render = require('render');
var events = require('events');
var bus = require('bus');
var classes = require('classes');
var template = require('./sidebar-container');
var filter = require('./filter');
var list = require('./list');
var lawsFilter = require('laws-filter');
var StatefulView = require('stateful-view');
var log = require('debug')('democracyos:sidebar:view');

/**
 * Expose SidebarView
 */

module.exports = SidebarView;

/**
 * Create Sidebar List view container
 */

function SidebarView() {
  if (!(this instanceof SidebarView)) {
    return new SidebarView();
  };

  StatefulView.call(this, template);
  this.filtercontainer = this.find('#filter-container');
  this.listcontainer = this.find('#list-container');
}

StatefulView(SidebarView);

SidebarView.prototype.switchOn = function() {
  filter.ready(this.bound('onfilterready'));
  bus.on('vote', this.vote);
}

SidebarView.prototype.switchOff = function() {
  bus.off('vote', this.vote);
}

SidebarView.prototype.onfilterready = function() {
  filter.appendTo(this.filtercontainer[0]);
  list.ready(this.onlistready.bind(this));
};

SidebarView.prototype.onlistready = function() {
  list.appendTo(this.listcontainer[0]);
  this.state('loaded');
};

SidebarView.prototype.vote = function (id) {
  list.vote(id);
}

SidebarView.prototype.items = function(index) {
  return lawsFilter.items()[index];
};

SidebarView.prototype.select = function(id) {
  var parent = lawsFilter.getParent(id);
  var els = this.find('ul.nav.navlist li');
  var el = this.find('ul.nav.navlist li[data-id="' + id + '"]');

  els.removeClass('active');
  els.removeClass('hide');
  el.addClass('active');

  if (parent) {
    var other = this.find('ul.nav.navlist li.parent:not([data-id="' + parent.id + '"])');
    other.addClass('hide');
  }
  return this;
}

SidebarView.prototype.selected = function() {
  if (!this.items()) return null;

  var el = this.el.querySelector('ul.nav.navlist li.active');
  var id = el ? el.getAttribute('data-id') : null;

  return id ? get(this.items(), 'id === "%id"'.replace('%id', id)) : null;
}

SidebarView.prototype.nextOf = function(id) {
  function parse(el) {
    var id = el.attr('data-id');
    var title = el.find('.title').html();
    return { id: id, title: title };
  }

  // Fetch the active element
  var el = this.listcontainer.find('ul.nav.navlist li[data-id="' + id + '"]');
  var children = el.find('.children');
  if (children.length > 0) {
    // If it has children, it is the parent, so the next is the first child
    var first = children.find('li:first-child');
    return parse(first);
  } else {
    // If it has not children, it is a child. I should get the next sibling, if it has one.
    var next = el.next();
    return next.length ? parse(next) : null;
  }
};

/**
 * Handle errors
 *
 * @param {String} error
 * @return {SidebarView} Instance of `SidebarView`
 * @api public
 */

SidebarView.prototype.error = function(message) {
  // TODO: We should use `Error`s instead of
  // `Strings` to handle errors...
  // Ref: http://www.devthought.com/2011/12/22/a-string-is-not-an-error/
  this.state('error', message);
  log('error found: %s', message);

  // Unregister all `ready` listeners
  this.off('ready');
  return this;
}

function get(list, query) {
  var match;
  var test = new Function('_', 'return _.' + query);

  list.some(function(l) {
    if (test(l)) {
      match = l;
      return true;
    };
    return false;
  });
  return match || null;
}<commit_msg>[sidebar] Fixed next button behavior on Introduction topic. Closes #22<commit_after>/**
 * Module dependencies.
 */

var Emitter = require('emitter');
var render = require('render');
var events = require('events');
var bus = require('bus');
var classes = require('classes');
var template = require('./sidebar-container');
var filter = require('./filter');
var list = require('./list');
var lawsFilter = require('laws-filter');
var StatefulView = require('stateful-view');
var log = require('debug')('democracyos:sidebar:view');

/**
 * Expose SidebarView
 */

module.exports = SidebarView;

/**
 * Create Sidebar List view container
 */

function SidebarView() {
  if (!(this instanceof SidebarView)) {
    return new SidebarView();
  };

  StatefulView.call(this, template);
  this.filtercontainer = this.find('#filter-container');
  this.listcontainer = this.find('#list-container');
}

StatefulView(SidebarView);

SidebarView.prototype.switchOn = function() {
  filter.ready(this.bound('onfilterready'));
  bus.on('vote', this.vote);
}

SidebarView.prototype.switchOff = function() {
  bus.off('vote', this.vote);
}

SidebarView.prototype.onfilterready = function() {
  filter.appendTo(this.filtercontainer[0]);
  list.ready(this.onlistready.bind(this));
};

SidebarView.prototype.onlistready = function() {
  list.appendTo(this.listcontainer[0]);
  this.state('loaded');
};

SidebarView.prototype.vote = function (id) {
  list.vote(id);
}

SidebarView.prototype.items = function(index) {
  return lawsFilter.items()[index];
};

SidebarView.prototype.select = function(id) {
  var parent = lawsFilter.getParent(id);
  var els = this.find('ul.nav.navlist li');
  var el = this.find('ul.nav.navlist li[data-id="' + id + '"]');

  els.removeClass('active');
  els.removeClass('hide');
  el.addClass('active');

  if (parent) {
    var other = this.find('ul.nav.navlist li.parent:not([data-id="' + parent.id + '"])');
    other.addClass('hide');
  }
  return this;
}

SidebarView.prototype.selected = function() {
  if (!this.items()) return null;

  var el = this.el.querySelector('ul.nav.navlist li.active');
  var id = el ? el.getAttribute('data-id') : null;

  return id ? get(this.items(), 'id === "%id"'.replace('%id', id)) : null;
}

SidebarView.prototype.nextOf = function(id) {
  function parse(el) {
    var id = el.attr('data-id');
    var title = el.find('.title').html();
    return { id: id, title: title };
  }

  // Fetch the active element
  var el = this.listcontainer.find('ul.nav.navlist .children li[data-id="' + id + '"]');
  var children = el.find('.children');
  if (children.length > 0) {
    // If it has children, it is the parent, so the next is the first child
    var first = children.find('li:first-child');
    return parse(first);
  } else {
    // If it has not children, it is a child. I should get the next sibling, if it has one.
    var next = el.next();
    return next.length ? parse(next) : null;
  }
};

/**
 * Handle errors
 *
 * @param {String} error
 * @return {SidebarView} Instance of `SidebarView`
 * @api public
 */

SidebarView.prototype.error = function(message) {
  // TODO: We should use `Error`s instead of
  // `Strings` to handle errors...
  // Ref: http://www.devthought.com/2011/12/22/a-string-is-not-an-error/
  this.state('error', message);
  log('error found: %s', message);

  // Unregister all `ready` listeners
  this.off('ready');
  return this;
}

function get(list, query) {
  var match;
  var test = new Function('_', 'return _.' + query);

  list.some(function(l) {
    if (test(l)) {
      match = l;
      return true;
    };
    return false;
  });
  return match || null;
}<|endoftext|>


<commit_before><commit_msg>fix jshint issues<commit_after><|endoftext|>


<commit_before>import Realm from 'realm';

import currency from '../../localization/currency';
import { createRecord } from '../utilities';

/**
 * A stocktake batch.
 *
 * @property  {string}     id
 * @property  {Stocktake}  stocktake
 * @property  {ItemBatch}  itemBatch
 * @property  {number}     snapshotNumberOfPacks
 * @property  {number}     packSize
 * @property  {Date}       expiryDate
 * @property  {string}     batch
 * @property  {number}     costPrice
 * @property  {number}     sellPrice
 * @property  {number}     countedNumberOfPacks
 * @property  {number}     sortIndex
 */
export class StocktakeBatch extends Realm.Object {
  /**
   * Delete stocktake batch and associated item batch.
   *
   * @param  {Realm}  database
   */
  destructor(database) {
    if (this.snapshotNumberOfPacks === 0 && this.itemBatch.numberOfPacks === 0) {
      database.delete('ItemBatch', this.itemBatch);
    }
  }

  /**
   * Get snapshot of total quantity in batch.
   *
   * @return  {number}
   */
  get snapshotTotalQuantity() {
    return this.snapshotNumberOfPacks * this.packSize;
  }

  /**
   * Get the total countedQuantity for this batch. If nothing has been
   * counted, return 0.
   *
   * @return  {number}
   */
  get countedTotalQuantity() {
    if (this.countedNumberOfPacks === null) return 0;
    return this.countedNumberOfPacks * this.packSize;
  }

  /**
   * Get if stocktake will reduce stock on hand of item below zero.
   *
   * Stock on hand should never be negative. This can occur when stock has been issued
   * in a customer invoice for this batch before the parent stocktake was finalised.
   *
   * @return  {boolean}
   */
  get isReducedBelowMinimum() {
    const { totalQuantity: stockOnHand } = this.itemBatch;

    // Return true if |stockOnHand + stocktakebatch.difference| is negative.
    return stockOnHand + this.difference < 0;
  }

  /**
   * Get if snapshot is out-of-date.
   *
   * Returns true if the |snapshotTotalQuantity| does not match the |totalQuantity| of the
   * item batch this stocktake batch is associated with. Will occur if inventory has changed
   * since this stocktake batch was created. If the net change on this batch is 0, then return
   * false.
   *
   * @return  {boolean}
   */
  get isSnapshotOutdated() {
    return this.snapshotTotalQuantity !== this.itemBatch.totalQuantity;
  }

  /**
   * Get if stocktake batch has been counted.
   *
   * @return  {boolean}
   */
  get hasBeenCounted() {
    return this.countedNumberOfPacks !== null;
  }

  /**
   * Get id of item associated with this stocktake batch.
   *
   * @return  {string}
   */
  get itemId() {
    if (!this.itemBatch) return '';
    return this.itemBatch.item ? this.itemBatch.item.id : '';
  }

  /**
   * Get id of item batch associated with this stocktake batch.
   *
   * @return  {string}
   */
  get itemBatchId() {
    return this.itemBatch ? this.itemBatch.id : '';
  }

  /**
   * Get difference between counted quantity and snapshop quantity.
   *
   * @return  {number}
   */
  get difference() {
    return this.countedTotalQuantity - this.snapshotTotalQuantity;
  }

  /**
   * Indicator that this batch has a positive difference in snapshot and
   * counted quantity.
   */
  get hasPositiveAdjustment() {
    return this.difference > 0;
  }

  /**
   * Indicator that this batch has a negative difference in snapshot and
   * counted quantity.
   */
  get hasNegativeAdjustment() {
    return this.difference < 0;
  }

  /**
   * Returns an indicator that this batches reason/option state is valid.
   * Valid being: negative differences require a negativeInventoryAdjustment
   * option. Positive differences require a positiveInventoryAdjustment option
   * while no difference requires there to be no option applied.
   */
  get validateReason() {
    // Short circuits for simple cases
    if (!this.difference && !this.option) return true;
    if (this.difference && !this.option) return false;

    // Determine the validity of the reason state where there is a difference
    // and the batch already has a option.
    const { type } = this.option;

    const positiveAdjustmentReason = type === 'positiveInventoryAdjustment';
    const negativeAdjustmentReason = type === 'negativeInventoryAdjustment';

    const correctPositiveReason = positiveAdjustmentReason && this.hasPositiveAdjustment;
    const correctNegativeReason = negativeAdjustmentReason && this.hasNegativeAdjustment;

    return correctNegativeReason || correctPositiveReason;
  }

  /**
   * @return {String} this batches reason title, or an empty string.
   */
  get reasonTitle() {
    return (this.option && this.option.title) || '';
  }

  /**
   * Set counted total quantity.
   *
   * @param  {number}  quantity
   */
  set countedTotalQuantity(quantity) {
    // Handle packsize of 0.
    this.countedNumberOfPacks = this.packSize ? quantity / this.packSize : 0;
  }

  /**
   * Removes a reason from this batch if it already has a reason and difference.
   * @param {Realm} database App-wide database interface.
   */
  removeReason(database) {
    if (this.option && this.difference) {
      database.write(() => {
        this.option = null;
        database.save('StocktakeBatch', this);
      });
    }
  }

  /**
   * Applies a reason to this batch after it has been validated as a valid reason
   *
   * @param {Realm} database App-wide database interface
   * @param {Option} newOption New option to apply
   */
  applyReason(database, newOption) {
    const { type: newOptionType } = newOption || {};

    const isPositiveAdjustmentReason = newOptionType === 'positiveInventoryAdjustment';
    const isNegativeAdjustmentReason = newOptionType === 'negativeInventoryAdjustment';

    // Valid adjustments are when this batch has a difference in snapshot quantity and
    // counted quantity and if the difference is positive, the reason must be a positive
    // reason also (and vice-versa for negatives)
    const isValidPositiveAdjustment =
      !!this.difference && this.hasPositiveAdjustment && isPositiveAdjustmentReason;
    const isValidNegativeAdjustment =
      !!this.difference && this.hasNegativeAdjustment && isNegativeAdjustmentReason;

    database.write(() => {
      database.update('StocktakeBatch', {
        ...this,
        option: isValidPositiveAdjustment || isValidNegativeAdjustment ? newOption : null,
      });
    });
  }

  /**
   * Finalise stocktake batch.
   *
   * Finalising will adjust inventory appropriately and will add a new transaction
   * batch in reducing or increasing transaction for this stocktake.
   *
   * @param  {Realm}  database
   */
  finalise(database) {
    // Update the item batch details.
    this.itemBatch.batch = this.batch;
    this.itemBatch.expiryDate = this.expiryDate;
    this.itemBatch.sellPrice = this.sellPrice;

    // Make inventory adjustments if there is a difference to apply.
    if (this.difference !== 0) {
      const isAddition = this.countedTotalQuantity > this.snapshotTotalQuantity;
      const inventoryAdjustment = isAddition
        ? this.stocktake.getAdditions(database)
        : this.stocktake.getReductions(database);

      // Create transaction item and transaction batch to store inventory adjustments in this
      // stocktake.
      const transactionItem = createRecord(
        database,
        'TransactionItem',
        inventoryAdjustment,
        this.itemBatch.item
      );
      const transactionBatch = createRecord(
        database,
        'TransactionBatch',
        transactionItem,
        this.itemBatch,
        isAddition
      );

      // Apply difference from stocktake to actual stock on hand levels. Whether stock is increased
      // or decreased is determined by the transaction, use the absolute value of difference
      // (i.e. always treat as positive).
      const snapshotDifference = Math.abs(this.difference);
      transactionBatch.setTotalQuantity(database, snapshotDifference);
      database.save('TransactionBatch', transactionBatch);
    }
    database.save('ItemBatch', this.itemBatch);
  }

  /**
   * Get string representation of stocktake batch.
   *
   * @return  {string}
   */
  toString() {
    return `Stocktake batch representing ${this.itemBatch}`;
  }

  get costPriceString() {
    return currency(this.costPrice ?? 0, { formatWithSymbol: true }).format();
  }

  get sellPriceString() {
    return currency(this.sellPrice ?? 0, { formatWithSymbol: true }).format();
  }
}

StocktakeBatch.schema = {
  name: 'StocktakeBatch',
  primaryKey: 'id',
  properties: {
    id: 'string',
    stocktake: 'Stocktake',
    itemBatch: 'ItemBatch',
    snapshotNumberOfPacks: 'double',
    packSize: 'double',
    expiryDate: { type: 'date', optional: true },
    batch: 'string',
    costPrice: 'double',
    sellPrice: 'double',
    countedNumberOfPacks: { type: 'double', optional: true },
    sortIndex: { type: 'int', optional: true },
    option: { type: 'Options', optional: true },
  },
};

export default StocktakeBatch;
<commit_msg>Add supplier field to StocktakeBatch<commit_after>import Realm from 'realm';

import currency from '../../localization/currency';
import { createRecord } from '../utilities';

/**
 * A stocktake batch.
 *
 * @property  {string}     id
 * @property  {Stocktake}  stocktake
 * @property  {ItemBatch}  itemBatch
 * @property  {number}     snapshotNumberOfPacks
 * @property  {number}     packSize
 * @property  {Date}       expiryDate
 * @property  {string}     batch
 * @property  {number}     costPrice
 * @property  {number}     sellPrice
 * @property  {number}     countedNumberOfPacks
 * @property  {number}     sortIndex
 */
export class StocktakeBatch extends Realm.Object {
  /**
   * Delete stocktake batch and associated item batch.
   *
   * @param  {Realm}  database
   */
  destructor(database) {
    if (this.snapshotNumberOfPacks === 0 && this.itemBatch.numberOfPacks === 0) {
      database.delete('ItemBatch', this.itemBatch);
    }
  }

  get otherPartyName() {
    return this.supplier?.name ?? '';
  }

  /**
   * Get snapshot of total quantity in batch.
   *
   * @return  {number}
   */
  get snapshotTotalQuantity() {
    return this.snapshotNumberOfPacks * this.packSize;
  }

  /**
   * Get the total countedQuantity for this batch. If nothing has been
   * counted, return 0.
   *
   * @return  {number}
   */
  get countedTotalQuantity() {
    if (this.countedNumberOfPacks === null) return 0;
    return this.countedNumberOfPacks * this.packSize;
  }

  /**
   * Get if stocktake will reduce stock on hand of item below zero.
   *
   * Stock on hand should never be negative. This can occur when stock has been issued
   * in a customer invoice for this batch before the parent stocktake was finalised.
   *
   * @return  {boolean}
   */
  get isReducedBelowMinimum() {
    const { totalQuantity: stockOnHand } = this.itemBatch;

    // Return true if |stockOnHand + stocktakebatch.difference| is negative.
    return stockOnHand + this.difference < 0;
  }

  /**
   * Get if snapshot is out-of-date.
   *
   * Returns true if the |snapshotTotalQuantity| does not match the |totalQuantity| of the
   * item batch this stocktake batch is associated with. Will occur if inventory has changed
   * since this stocktake batch was created. If the net change on this batch is 0, then return
   * false.
   *
   * @return  {boolean}
   */
  get isSnapshotOutdated() {
    return this.snapshotTotalQuantity !== this.itemBatch.totalQuantity;
  }

  /**
   * Get if stocktake batch has been counted.
   *
   * @return  {boolean}
   */
  get hasBeenCounted() {
    return this.countedNumberOfPacks !== null;
  }

  /**
   * Get id of item associated with this stocktake batch.
   *
   * @return  {string}
   */
  get itemId() {
    if (!this.itemBatch) return '';
    return this.itemBatch.item ? this.itemBatch.item.id : '';
  }

  /**
   * Get id of item batch associated with this stocktake batch.
   *
   * @return  {string}
   */
  get itemBatchId() {
    return this.itemBatch ? this.itemBatch.id : '';
  }

  /**
   * Get difference between counted quantity and snapshop quantity.
   *
   * @return  {number}
   */
  get difference() {
    return this.countedTotalQuantity - this.snapshotTotalQuantity;
  }

  /**
   * Indicator that this batch has a positive difference in snapshot and
   * counted quantity.
   */
  get hasPositiveAdjustment() {
    return this.difference > 0;
  }

  /**
   * Indicator that this batch has a negative difference in snapshot and
   * counted quantity.
   */
  get hasNegativeAdjustment() {
    return this.difference < 0;
  }

  /**
   * Returns an indicator that this batches reason/option state is valid.
   * Valid being: negative differences require a negativeInventoryAdjustment
   * option. Positive differences require a positiveInventoryAdjustment option
   * while no difference requires there to be no option applied.
   */
  get validateReason() {
    // Short circuits for simple cases
    if (!this.difference && !this.option) return true;
    if (this.difference && !this.option) return false;

    // Determine the validity of the reason state where there is a difference
    // and the batch already has a option.
    const { type } = this.option;

    const positiveAdjustmentReason = type === 'positiveInventoryAdjustment';
    const negativeAdjustmentReason = type === 'negativeInventoryAdjustment';

    const correctPositiveReason = positiveAdjustmentReason && this.hasPositiveAdjustment;
    const correctNegativeReason = negativeAdjustmentReason && this.hasNegativeAdjustment;

    return correctNegativeReason || correctPositiveReason;
  }

  /**
   * @return {String} this batches reason title, or an empty string.
   */
  get reasonTitle() {
    return (this.option && this.option.title) || '';
  }

  /**
   * Set counted total quantity.
   *
   * @param  {number}  quantity
   */
  set countedTotalQuantity(quantity) {
    // Handle packsize of 0.
    this.countedNumberOfPacks = this.packSize ? quantity / this.packSize : 0;
  }

  /**
   * Removes a reason from this batch if it already has a reason and difference.
   * @param {Realm} database App-wide database interface.
   */
  removeReason(database) {
    if (this.option && this.difference) {
      database.write(() => {
        this.option = null;
        database.save('StocktakeBatch', this);
      });
    }
  }

  /**
   * Applies a reason to this batch after it has been validated as a valid reason
   *
   * @param {Realm} database App-wide database interface
   * @param {Option} newOption New option to apply
   */
  applyReason(database, newOption) {
    const { type: newOptionType } = newOption || {};

    const isPositiveAdjustmentReason = newOptionType === 'positiveInventoryAdjustment';
    const isNegativeAdjustmentReason = newOptionType === 'negativeInventoryAdjustment';

    // Valid adjustments are when this batch has a difference in snapshot quantity and
    // counted quantity and if the difference is positive, the reason must be a positive
    // reason also (and vice-versa for negatives)
    const isValidPositiveAdjustment =
      !!this.difference && this.hasPositiveAdjustment && isPositiveAdjustmentReason;
    const isValidNegativeAdjustment =
      !!this.difference && this.hasNegativeAdjustment && isNegativeAdjustmentReason;

    database.write(() => {
      database.update('StocktakeBatch', {
        ...this,
        option: isValidPositiveAdjustment || isValidNegativeAdjustment ? newOption : null,
      });
    });
  }

  /**
   * Finalise stocktake batch.
   *
   * Finalising will adjust inventory appropriately and will add a new transaction
   * batch in reducing or increasing transaction for this stocktake.
   *
   * @param  {Realm}  database
   */
  finalise(database) {
    // Update the item batch details.
    this.itemBatch.batch = this.batch;
    this.itemBatch.expiryDate = this.expiryDate;
    this.itemBatch.sellPrice = this.sellPrice;

    // Make inventory adjustments if there is a difference to apply.
    if (this.difference !== 0) {
      const isAddition = this.countedTotalQuantity > this.snapshotTotalQuantity;
      const inventoryAdjustment = isAddition
        ? this.stocktake.getAdditions(database)
        : this.stocktake.getReductions(database);

      // Create transaction item and transaction batch to store inventory adjustments in this
      // stocktake.
      const transactionItem = createRecord(
        database,
        'TransactionItem',
        inventoryAdjustment,
        this.itemBatch.item
      );
      const transactionBatch = createRecord(
        database,
        'TransactionBatch',
        transactionItem,
        this.itemBatch,
        isAddition
      );

      // Apply difference from stocktake to actual stock on hand levels. Whether stock is increased
      // or decreased is determined by the transaction, use the absolute value of difference
      // (i.e. always treat as positive).
      const snapshotDifference = Math.abs(this.difference);
      transactionBatch.setTotalQuantity(database, snapshotDifference);
      database.save('TransactionBatch', transactionBatch);
    }
    database.save('ItemBatch', this.itemBatch);
  }

  /**
   * Get string representation of stocktake batch.
   *
   * @return  {string}
   */
  toString() {
    return `Stocktake batch representing ${this.itemBatch}`;
  }

  get costPriceString() {
    return currency(this.costPrice ?? 0, { formatWithSymbol: true }).format();
  }

  get sellPriceString() {
    return currency(this.sellPrice ?? 0, { formatWithSymbol: true }).format();
  }
}

StocktakeBatch.schema = {
  name: 'StocktakeBatch',
  primaryKey: 'id',
  properties: {
    id: 'string',
    stocktake: 'Stocktake',
    itemBatch: 'ItemBatch',
    snapshotNumberOfPacks: 'double',
    packSize: 'double',
    expiryDate: { type: 'date', optional: true },
    batch: 'string',
    costPrice: 'double',
    sellPrice: 'double',
    countedNumberOfPacks: { type: 'double', optional: true },
    sortIndex: { type: 'int', optional: true },
    option: { type: 'Options', optional: true },
    supplier: { type: 'Name', optional: true },
  },
};

export default StocktakeBatch;
<|endoftext|>


<commit_before>// databankobject.js
//
// abstraction for CRUD'ing an object as JSON to a Databank
//
// Copyright 2011, 2012 StatusNet Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

var Step = require('step'),
    Databank = require('./databank').Databank;

var DatabankObject = function(properties) {
    DatabankObject.init(this, properties);
};

DatabankObject.init = function(obj, properties) {
    DatabankObject.copy(obj, properties);
};

DatabankObject.copy = function(obj, properties) {
    var property;
    for (property in properties) {
        obj[property] = properties[property];
    }
};

DatabankObject.driver = null;
DatabankObject.params = {};
DatabankObject.bank = null;

// Utilities for default hooks

var passThru0 = function(callback) {
    callback(null);
};

var passThru1 = function(arg, callback) {
    callback(null, arg);
};

// Default hooks

DatabankObject.beforeCreate = passThru1;
DatabankObject.prototype.afterCreate = passThru0;

DatabankObject.beforeGet = passThru1;
DatabankObject.prototype.afterGet = passThru0;

DatabankObject.prototype.beforeUpdate = passThru1;
DatabankObject.prototype.afterUpdate = passThru0;

DatabankObject.prototype.beforeDel = passThru0;
DatabankObject.prototype.afterDel = passThru0;

DatabankObject.prototype.beforeSave = passThru0;
DatabankObject.prototype.afterSave = passThru0;

DatabankObject.subClass = function(type, Parent) {

    var Cls;

    if (!Parent) {
        Parent = DatabankObject;
    }

    Cls = function(properties) {
        Cls.init(this, properties);
    };

    Cls.init = function(inst, properties) {
        Parent.init(inst, properties);
    };

    Cls.parent = Parent;

    Cls.beforeCreate = Parent.beforeCreate || DatabankObject.beforeCreate;
    Cls.beforeGet = Parent.beforeGet || DatabankObject.beforeGet;

    Cls.bank = function() {
        return DatabankObject.bank;
    };

    Cls.type = type;

    Cls.get = function(id, callback) {
        Cls.beforeGet(id, function(err) {
            if (err) {
                callback(err, null);
            } else {
                Cls.bank().read(Cls.type, id, function(err, value) {
                    var c;
                    if (err) {
                        callback(err, null);
                    } else {
                        c = new Cls(value);
                        c.afterGet(function(err) {
                            if (err) {
                                callback(err, null);
                            } else {
                                callback(null, c);
                            }
                        });
                    }
                });
            }
        });
    };

    Cls.search = function(criteria, callback) {
        var results = [];

        Cls.bank().search(Cls.type,
                          criteria,
                          function(value) {
                              results.push(new Cls(value));
                          },
                          function(err) {
                              if (err) {
                                  callback(err, null);
                              } else {
                                  callback(null, results);
                              }
                          });
    };

    Cls.pkey = function() {
        var bank;
        if (Cls.schema && Cls.schema.pkey) {
            return Cls.schema.pkey;
        }
        if (Cls.schema && Cls.schema[Cls.type] && Cls.schema[Cls.type].pkey) {
            return Cls.schema[Cls.type].pkey;
        }
        bank = Cls.bank();
        if (bank && bank.schema && bank.schema[Cls.type] && bank.schema[Cls.type].pkey) {
            return bank.schema[Cls.type].pkey;
        }
        return "id";
    };

    Cls.create = function(orig, callback) {
        Cls.beforeCreate(orig, function(err, props) {
            if (err) {
                callback(err, null);
            } else {
                Cls.bank().create(Cls.type, props[Cls.pkey()], props, function(err, value) {
                    var inst;
                    if (err) {
                        callback(err, null);
                    } else {
                        inst = new Cls(value);
                        inst.afterCreate(function(err) {
                            if (err) {
                                callback(err, null);
                            } else {
                                callback(null, inst);
                            }
                        });
                    }
                });
            }
        });
    };

    Cls.readAll = function(ids, callback) {
        var clsMap;

        Step(
            function() {
                var i, group = this.group();
                for (i = 0; i < ids.length; i++) {
                    Cls.beforeGet(ids[i], group());
                }
            },
            function(err, results) {
                if (err) throw err;
                ids = results;
                Cls.bank().readAll(Cls.type, ids, this);
            },
            function(err, map) {
                var id, group = this.group();
                if (err) throw err;
                for (id in map) {
                    if (map.hasOwnProperty(id)) {
                        if (map[id]) {
                            clsMap[id] = new Cls(map[id]);
                            clsMap[id].afterGet(group());
                        } else {
                            clsMap[id] = null;
                            group()(null);
                        }
                    }
                }
            },
            function(err) {
                if (err) {
                    callback(err, null);
                } else {
                    callback(null, clsMap);
                }
            }
        );
    };

    Cls.prototype = new Parent({
        update: function(orig, callback) {
            var inst = this;
            inst.beforeUpdate(orig, function(err, props) {
                if (err) {
                    callback(err, null);
                } else {
                    DatabankObject.copy(inst, props);
                    Cls.bank().update(Cls.type, inst[Cls.pkey()], inst, function(err, value) {
                        if (err) {
                            callback(err, null);
                        } else {
                            DatabankObject.copy(inst, value); // may be updated
                            inst.afterUpdate(function(err) {
                                if (err) {
                                    callback(err, null);
                                } else {
                                    callback(null, inst);
                                }
                            });
                        }
                    });
                }
            });
        },
        del: function(callback) {
            var inst = this;
            inst.beforeDel(function(err) {
                if (err) {
                    callback(err);
                } else {
                    Cls.bank().del(Cls.type, inst[Cls.pkey()], function(err) {
                        if (err) {
                            callback(err);
                        } else {
                            inst.afterDel(callback);
                        }
                    });
                }
            });
        },
        save: function(callback) {
            var inst = this;
            inst.beforeSave(function(err) {
                if (err) {
                    callback(err);
                } else {
                    Cls.bank().save(Cls.type, inst[Cls.pkey()], inst, function(err, value) {
                        if (err) {
                            callback(err, null);
                        } else {
                            DatabankObject.copy(inst, value); // may be updated
                            inst.afterSave(function(err) {
                                if (err) {
                                    callback(err, null);
                                } else {
                                    callback(null, inst);
                                }
                            });
                        }
                    });
                }
            });
        }
    });

    return Cls;
};

exports.DatabankObject = DatabankObject;
<commit_msg>Init clsMap object<commit_after>// databankobject.js
//
// abstraction for CRUD'ing an object as JSON to a Databank
//
// Copyright 2011, 2012 StatusNet Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

var Step = require('step'),
    Databank = require('./databank').Databank;

var DatabankObject = function(properties) {
    DatabankObject.init(this, properties);
};

DatabankObject.init = function(obj, properties) {
    DatabankObject.copy(obj, properties);
};

DatabankObject.copy = function(obj, properties) {
    var property;
    for (property in properties) {
        obj[property] = properties[property];
    }
};

DatabankObject.driver = null;
DatabankObject.params = {};
DatabankObject.bank = null;

// Utilities for default hooks

var passThru0 = function(callback) {
    callback(null);
};

var passThru1 = function(arg, callback) {
    callback(null, arg);
};

// Default hooks

DatabankObject.beforeCreate = passThru1;
DatabankObject.prototype.afterCreate = passThru0;

DatabankObject.beforeGet = passThru1;
DatabankObject.prototype.afterGet = passThru0;

DatabankObject.prototype.beforeUpdate = passThru1;
DatabankObject.prototype.afterUpdate = passThru0;

DatabankObject.prototype.beforeDel = passThru0;
DatabankObject.prototype.afterDel = passThru0;

DatabankObject.prototype.beforeSave = passThru0;
DatabankObject.prototype.afterSave = passThru0;

DatabankObject.subClass = function(type, Parent) {

    var Cls;

    if (!Parent) {
        Parent = DatabankObject;
    }

    Cls = function(properties) {
        Cls.init(this, properties);
    };

    Cls.init = function(inst, properties) {
        Parent.init(inst, properties);
    };

    Cls.parent = Parent;

    Cls.beforeCreate = Parent.beforeCreate || DatabankObject.beforeCreate;
    Cls.beforeGet = Parent.beforeGet || DatabankObject.beforeGet;

    Cls.bank = function() {
        return DatabankObject.bank;
    };

    Cls.type = type;

    Cls.get = function(id, callback) {
        Cls.beforeGet(id, function(err) {
            if (err) {
                callback(err, null);
            } else {
                Cls.bank().read(Cls.type, id, function(err, value) {
                    var c;
                    if (err) {
                        callback(err, null);
                    } else {
                        c = new Cls(value);
                        c.afterGet(function(err) {
                            if (err) {
                                callback(err, null);
                            } else {
                                callback(null, c);
                            }
                        });
                    }
                });
            }
        });
    };

    Cls.search = function(criteria, callback) {
        var results = [];

        Cls.bank().search(Cls.type,
                          criteria,
                          function(value) {
                              results.push(new Cls(value));
                          },
                          function(err) {
                              if (err) {
                                  callback(err, null);
                              } else {
                                  callback(null, results);
                              }
                          });
    };

    Cls.pkey = function() {
        var bank;
        if (Cls.schema && Cls.schema.pkey) {
            return Cls.schema.pkey;
        }
        if (Cls.schema && Cls.schema[Cls.type] && Cls.schema[Cls.type].pkey) {
            return Cls.schema[Cls.type].pkey;
        }
        bank = Cls.bank();
        if (bank && bank.schema && bank.schema[Cls.type] && bank.schema[Cls.type].pkey) {
            return bank.schema[Cls.type].pkey;
        }
        return "id";
    };

    Cls.create = function(orig, callback) {
        Cls.beforeCreate(orig, function(err, props) {
            if (err) {
                callback(err, null);
            } else {
                Cls.bank().create(Cls.type, props[Cls.pkey()], props, function(err, value) {
                    var inst;
                    if (err) {
                        callback(err, null);
                    } else {
                        inst = new Cls(value);
                        inst.afterCreate(function(err) {
                            if (err) {
                                callback(err, null);
                            } else {
                                callback(null, inst);
                            }
                        });
                    }
                });
            }
        });
    };

    Cls.readAll = function(ids, callback) {
        var clsMap = {};

        Step(
            function() {
                var i, group = this.group();
                for (i = 0; i < ids.length; i++) {
                    Cls.beforeGet(ids[i], group());
                }
            },
            function(err, results) {
                if (err) throw err;
                ids = results;
                Cls.bank().readAll(Cls.type, ids, this);
            },
            function(err, map) {
                var id, group = this.group();
                if (err) throw err;
                for (id in map) {
                    if (map.hasOwnProperty(id)) {
                        if (map[id]) {
                            clsMap[id] = new Cls(map[id]);
                            clsMap[id].afterGet(group());
                        } else {
                            clsMap[id] = null;
                            group()(null);
                        }
                    }
                }
            },
            function(err) {
                if (err) {
                    callback(err, null);
                } else {
                    callback(null, clsMap);
                }
            }
        );
    };

    Cls.prototype = new Parent({
        update: function(orig, callback) {
            var inst = this;
            inst.beforeUpdate(orig, function(err, props) {
                if (err) {
                    callback(err, null);
                } else {
                    DatabankObject.copy(inst, props);
                    Cls.bank().update(Cls.type, inst[Cls.pkey()], inst, function(err, value) {
                        if (err) {
                            callback(err, null);
                        } else {
                            DatabankObject.copy(inst, value); // may be updated
                            inst.afterUpdate(function(err) {
                                if (err) {
                                    callback(err, null);
                                } else {
                                    callback(null, inst);
                                }
                            });
                        }
                    });
                }
            });
        },
        del: function(callback) {
            var inst = this;
            inst.beforeDel(function(err) {
                if (err) {
                    callback(err);
                } else {
                    Cls.bank().del(Cls.type, inst[Cls.pkey()], function(err) {
                        if (err) {
                            callback(err);
                        } else {
                            inst.afterDel(callback);
                        }
                    });
                }
            });
        },
        save: function(callback) {
            var inst = this;
            inst.beforeSave(function(err) {
                if (err) {
                    callback(err);
                } else {
                    Cls.bank().save(Cls.type, inst[Cls.pkey()], inst, function(err, value) {
                        if (err) {
                            callback(err, null);
                        } else {
                            DatabankObject.copy(inst, value); // may be updated
                            inst.afterSave(function(err) {
                                if (err) {
                                    callback(err, null);
                                } else {
                                    callback(null, inst);
                                }
                            });
                        }
                    });
                }
            });
        }
    });

    return Cls;
};

exports.DatabankObject = DatabankObject;
<|endoftext|>


<commit_before>/** @namespace */
var C64Style = C64Style || {};

/** The Screen is the overriding container for all C64Style components.
* The Screen orchestrates updating and rendering its layers, propagates
* mouse events down to the layers, and notifies event listeners when events occur.
* @constructor
* @param {HTMLElement} targetDiv The target HTMLElement into which the screen and its layers will be built.
* @param {Object} config Supported configuration properties:
*

*
scaleX - integer - the horizontal scale of the screen.  Default: 1

*
scaleY - integer - the horizontal scale of the screen.  Default: 1

*
cols - integer - The number of columns for the screen.  Width will be sized accordingly: cols * {@link C64Style.CELLWIDTH}.  Default: 40

*
rows - integer - The number of rows for the screen.  Height will be sized accordingly: rows * {@link C64Style.CELLHEIGHT}.  Default: 25

*
fpsElem - HTMLElement - Element to write Frames-per-second information to.

*
backgroundColor - Color - The color to set the screen background to. Default: C64Style.Color.BLUE

*
borderColor - Color - The color to set the screen border to. Default: C64Style.Color.LIGHTBLUE

*
borderSize - integer - The borderSize of the screen, in pixels. Default: 20

*

*/
C64Style.C64Screen = function(targetDiv, layerFactory, config) {
  SL.Screen.call(this, targetDiv, layerFactory, config);

  this._cols = this._config.cols || 40;
  this._rows = this._config.rows || 25;
  this._width = this._cols * this._scaleX * C64Style.CELLWIDTH;
  this._height = this._rows * this._scaleY * C64Style.CELLHEIGHT;

  this._mouseRow = -1;
  this._mouseCol = -1;

  this._backgroundColor = this._config.backgroundColor || C64Style.Color.BLUE;
  this._borderColor = this._config.borderColor || C64Style.Color.LIGHTBLUE;
  this._borderSize = this._config.borderSize || 20;
};

C64Style.C64Screen.prototype = new SL.Screen();
C64Style.C64Screen.prototype.constructor = C64Style.C64Screen;

/** Return the number of rows.
* @returns {integer}
*/
C64Style.C64Screen.prototype.getRows = function() {return this._rows;};

/** Return the number of columns.
* @returns {integer}
*/
C64Style.C64Screen.prototype.getCols = function() {return this._cols;};

/** Return the current row coordinate of the mouse.
* @returns {integer}
*/
C64Style.C64Screen.prototype.getMouseRow = function() {return this._mouseRow;};

/** Return the current column coordinate of the mouse.
* @returns {integer}
*/
C64Style.C64Screen.prototype.getMouseCol = function() {return this._mouseCol;};

/** @private */
C64Style.C64Screen.prototype._handleMouseMoveEvent = function(time) {
  var event = new SL.Event(
    SL.EventType.MOUSE_MOVE,
    {
      x : this.getUnScaledX(this._mouseX),
      y : this.getUnScaledX(this._mouseY),
      row : this._mouseRow,
      col : this._mouseCol,
      scaledX : this._mouseX,
      scaledY : this._mouseY,
    },
    time
  );
  this.notify(event);
  this.propagateMouseEventThroughLayers(event);
  this._mouseMoved = false;
};

/** Handle a mouse move event.  This does not directly propagate the event to
* layers and elements; rather it will flag that a mouse movement has occured, and records its current location.
* The event will be propagated during the next render cycle.
* @param {Event} e The mouse event
*/
C64Style.C64Screen.prototype.handleMouseMoveEvent = function(e) {
  if (this._paused) return false;
  this._mouseMoved = true;
  var x = this.getXFromMouseEvent(e);
  var y = this.getYFromMouseEvent(e);

  if (x < 0 || x >= this._width || y < 0 || y >= this._height) {
    this._mouseX = -1;
    this._mouseY = -1;
    this._mouseRow = -1;
    this._mouseCol = -1;
    return false;
  }
  var row = this.getRowFromMouseEvent(e);
  var col = this.getColFromMouseEvent(e);
  this._mouseX = x;
  this._mouseY = y;
  this._mouseRow = row;
  this._mouseCol = col;
};

/** Handles mouse up and mouse down events; notifies any local handlers and propagates the event to all layers.
* @param {Event} e The mouse event
*/
C64Style.C64Screen.prototype.handleMouseEvent = function(e) {
  if (this._paused) return false;
  var scaledX = this.getXFromMouseEvent(e);
  var scaledY = this.getYFromMouseEvent(e);

  if (scaledX < 0 || scaledX >= this._width || scaledY < 0 || scaledY >= this._height) {
    return false;
  }
  var row = this.getRowFromMouseEvent(e);
  var col = this.getColFromMouseEvent(e);
  var x = this.getUnScaledX(scaledX);
  var y = this.getUnScaledY(scaledY);

  var type = e.type === "mouseup" ? SL.EventType.MOUSE_UP : SL.EventType.MOUSE_DOWN;
  var event = new SL.Event(
    type,
    {
      x : x,
      y : y,
      row : row,
      col : col,
      baseEvent : e,
      scaledX : scaledX,
      scaledY : scaledY
    });
  this.notify(event);

  // propagate through layers
  this.propagateMouseEventThroughLayers(event);

  if (e.button === 1) return false;
};

/** Return the column coordinate from a mouse event.  Accounts for screen position.
* @param {Event} e Mouse Event
*/
C64Style.C64Screen.prototype.getColFromMouseEvent = function(e) {
  return Math.floor(this.getXFromMouseEvent(e) / (C64Style.CELLWIDTH * this._scaleX));
};

/** Return the row coordinate from a mouse event.  Accounts for screen position.
* @param {Event} e Mouse Event
*/
C64Style.C64Screen.prototype.getRowFromMouseEvent = function(e) {
  return Math.floor(this.getYFromMouseEvent(e) / (C64Style.CELLHEIGHT * this._scaleY));
};

C64Style.C64Screen.prototype.columnToX = function(col) {
  var x = col * this.getScaleX() * C64Style.CELLWIDTH;
  return x;
};
C64Style.C64Screen.prototype.xToColumn = function(x) {
  var col = x / (this.getScaleX() * C64Style.CELLWIDTH);
  return col;
};

C64Style.C64Screen.prototype.rowToY = function(row) {
  var y = row * this.getScaleY() * C64Style.CELLHEIGHT;
  return y;
};
C64Style.C64Screen.prototype.yToRow = function(y) {
  var row = y / (this.getScaleY() * C64Style.CELLHEIGHT);
  return row;
};
<commit_msg>Removing last commit, it was borked<commit_after>/** @namespace */
var C64Style = C64Style || {};

/** The Screen is the overriding container for all C64Style components.
* The Screen orchestrates updating and rendering its layers, propagates
* mouse events down to the layers, and notifies event listeners when events occur.
* @constructor
* @param {HTMLElement} targetDiv The target HTMLElement into which the screen and its layers will be built.
* @param {Object} config Supported configuration properties:
*

*
scaleX - integer - the horizontal scale of the screen.  Default: 1

*
scaleY - integer - the horizontal scale of the screen.  Default: 1

*
cols - integer - The number of columns for the screen.  Width will be sized accordingly: cols * {@link C64Style.CELLWIDTH}.  Default: 40

*
rows - integer - The number of rows for the screen.  Height will be sized accordingly: rows * {@link C64Style.CELLHEIGHT}.  Default: 25

*
fpsElem - HTMLElement - Element to write Frames-per-second information to.

*
backgroundColor - Color - The color to set the screen background to. Default: C64Style.Color.BLUE

*
borderColor - Color - The color to set the screen border to. Default: C64Style.Color.LIGHTBLUE

*
borderSize - integer - The borderSize of the screen, in pixels. Default: 20

*

*/
C64Style.C64Screen = function(targetDiv, layerFactory, config) {
  SL.Screen.call(this, targetDiv, layerFactory, config);

  this._cols = this._config.cols || 40;
  this._rows = this._config.rows || 25;
  this._width = this._cols * this._scaleX * C64Style.CELLWIDTH;
  this._height = this._rows * this._scaleY * C64Style.CELLHEIGHT;

  this._mouseRow = -1;
  this._mouseCol = -1;

  this._backgroundColor = this._config.backgroundColor || C64Style.Color.BLUE;
  this._borderColor = this._config.borderColor || C64Style.Color.LIGHTBLUE;
  this._borderSize = this._config.borderSize || 20;
};

C64Style.C64Screen.prototype = new SL.Screen();
C64Style.C64Screen.prototype.constructor = C64Style.C64Screen;

/** Return the number of rows.
* @returns {integer}
*/
C64Style.C64Screen.prototype.getRows = function() {return this._rows;};

/** Return the number of columns.
* @returns {integer}
*/
C64Style.C64Screen.prototype.getCols = function() {return this._cols;};

/** Return the current row coordinate of the mouse.
* @returns {integer}
*/
C64Style.C64Screen.prototype.getMouseRow = function() {return this._mouseRow;};

/** Return the current column coordinate of the mouse.
* @returns {integer}
*/
C64Style.C64Screen.prototype.getMouseCol = function() {return this._mouseCol;};

/** @private */
C64Style.C64Screen.prototype._handleMouseMoveEvent = function(time) {
  var event = new SL.Event(
    SL.EventType.MOUSE_MOVE,
    {
      x : this.getUnScaledX(this._mouseX),
      y : this.getUnScaledX(this._mouseY),
      row : this._mouseRow,
      col : this._mouseCol,
      scaledX : this._mouseX,
      scaledY : this._mouseY,
    },
    time
  );
  this.notify(event);
  this.propagateMouseEventThroughLayers(event);
  this._mouseMoved = false;
};

/** Handle a mouse move event.  This does not directly propagate the event to
* layers and elements; rather it will flag that a mouse movement has occured, and records its current location.
* The event will be propagated during the next render cycle.
* @param {Event} e The mouse event
*/
C64Style.C64Screen.prototype.handleMouseMoveEvent = function(e) {
  if (this._paused) return false;
  this._mouseMoved = true;
  var x = this.getXFromMouseEvent(e);
  var y = this.getYFromMouseEvent(e);

  if (x < 0 || x >= this._width || y < 0 || y >= this._height) {
    this._mouseX = -1;
    this._mouseY = -1;
    this._mouseRow = -1;
    this._mouseCol = -1;
    return false;
  }
  var row = this.getRowFromMouseEvent(e);
  var col = this.getColFromMouseEvent(e);
  this._mouseX = x;
  this._mouseY = y;
  this._mouseRow = row;
  this._mouseCol = col;
};

/** Handles mouse up and mouse down events; notifies any local handlers and propagates the event to all layers.
* @param {Event} e The mouse event
*/
C64Style.C64Screen.prototype.handleMouseEvent = function(e) {
  if (this._paused) return false;
  var scaledX = this.getXFromMouseEvent(e);
  var scaledY = this.getYFromMouseEvent(e);

  if (scaledX < 0 || scaledX >= this._width || scaledY < 0 || scaledY >= this._height) {
    return false;
  }
  var row = this.getRowFromMouseEvent(e);
  var col = this.getColFromMouseEvent(e);
  var x = this.getUnScaledX(scaledX);
  var y = this.getUnScaledY(scaledY);

  var type = e.type === "mouseup" ? SL.EventType.MOUSE_UP : SL.EventType.MOUSE_DOWN;
  var event = new SL.Event(
    type,
    {
      x : x,
      y : y,
      row : row,
      col : col,
      baseEvent : e,
      scaledX : scaledX,
      scaledY : scaledY
    });
  this.notify(event);

  // propagate through layers
  this.propagateMouseEventThroughLayers(event);

  if (e.button === 1) return false;
};

/** Return the column coordinate from a mouse event.  Accounts for screen position.
* @param {Event} e Mouse Event
*/
C64Style.C64Screen.prototype.getColFromMouseEvent = function(e) {
  return Math.floor(this.getXFromMouseEvent(e) / (C64Style.CELLWIDTH * this._scaleX));
};

/** Return the row coordinate from a mouse event.  Accounts for screen position.
* @param {Event} e Mouse Event
*/
C64Style.C64Screen.prototype.getRowFromMouseEvent = function(e) {
  return Math.floor(this.getYFromMouseEvent(e) / (C64Style.CELLHEIGHT * this._scaleY));
};
<|endoftext|>


<commit_before>/**
 * Extend module's NODE_PATH
 * HACK: temporary solution
 */

require('node-path')(module);

/**
 * Module dependencies.
 */

var mongoose = require('mongoose');
var Comment = mongoose.model('Comment');
var utils = require('lib/utils');
var pluck = utils.pluck;
var config = require('lib/config');
var t = require('t-component');
var log = require('debug')('democracyos:db-api:comment');
var config = require('lib/config');
var utils = require('lib/utils');
var notifier = require('lib/notifications').notifier;
var User = require('lib/models').User;

/**
 * Get all comments
 *
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @return {Module} `comment` module
 * @api public
 */

exports.all = function all(fn) {
  log('Looking for all comments.')

  Comment.find(function (err, comments) {
    if (err) {
      log('Found error %j', err);
      return fn(err);
    };

    log('Delivering comments %j', pluck(comments, 'id'));
    fn(null, comments);
  });

  return this;
};

/**
 * Create comment for `proposal` by `author`
 * with `text`
 *
 * @param {String} proposal to submit comment
 * @param {Object} comment comment vars like `text` and `author`
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @api public
 */

exports.create = function create(comment, fn) {
  log('Creating new comment %j for %s %s', comment.text, comment.context, comment.reference);

  var comment = new Comment(comment);

  comment.save(function (err) {
    if (err) {
      log('Found error %s', err);
      return fn(err);
    };
    User.populate(comment, { path: 'author' }, function(err) {
      if (err) {
        log('Found error %s', err)
        return fn(err);
      };

      log('Delivering comment %j', comment.id);
      fn(null, comment);
    });
  });
};

/**
 * Get comments for proposal
 *
 * @param {String} proposal to get comments from
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @api public
 */

exports.getFor = function getFor(query, paging, fn) {
  log('Looking for comments for %s %s', query.context, query.reference);

  paging = paging || { page: 0, limit: config.commentsPerPage, sort: 'score', exclude_user: null };

  Comment
  .find(query)
  .sort(paging.sort || 'score')
  .skip(paging.page * paging.limit)
  .limit(paging.limit)
  .exec(function(err, comments) {
    if (err) {
      log('Found error %j', err);
      return fn(err);
    };

    var opts = { path: 'author', select: 'id firstName lastName fullName email avatar' };

    User.populate(comments, opts, function(err, comments) {
      if (err) {
        log('Found error %j', err);
        return fn(err);
      };

      log('Delivering comments %j', pluck(comments, 'id'));
      fn(null, comments);
    });
  });
};

/**
 * Get replies for comment
 *
 * @param {String} id
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @api public
 */

exports.replies = function replies(id, fn) {
  log('Looking for replies for comment %s', id);

  Comment
  .findOne({ _id: id })
  .exec(function(err, comment) {
    if (err) {
      log('Found error %j', err);
      return fn(err);
    };

    var opts = { path: 'replies.author', select: 'id firstName lastName fullName email avatar' };

    User.populate(comment, opts, function(err, comment) {
      if (err) {
        log('Found error %j', err);
        return fn(err);
      };

      var replies = comment && comment.replies ? comment.replies : [];

      log('Delivering replies %j', pluck(replies, 'id'));
      fn(null, replies);
    });
  });
};

/**
 * Reply to comment
 *
 * @param {String} commentId to attach reply
 * @param {Object} reply object with params
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @api public
 */

exports.reply = function reply(commentId, reply, fn) {
  log('Looking for comment %s to reply with %j', commentId, reply);

  Comment.findById(commentId, function(err, comment) {
    if (err) {
      log('Found error %j', err);
      return fn(err);
    };

    log('Creating reply %j for comment %j', reply, comment);
    var doc = comment.replies.create(reply);
    comment.replies.push(doc);

    comment.save(function(err, saved) {

      if (err) {
        log('Found error %j', err);
        return fn(err);
      };

      var opts = { path: 'replies.author', select: 'id firstName lastName fullName email avatar' };

      User.populate(comment, opts, function(err, comment) {
        if (err) {
          log('Found error %j', err);
          return fn(err);
        }

        if (comment.author != reply.author.id) {

          var eventName = 'comment-reply'

          var topicUrl = utils.buildUrl(config, { pathname: '/topic/' + comment.reference });

          var r = {
            id: doc.id,
            author: { id: reply.author.id },
            text: reply.text
          }

          var c = {
            id: comment.id,
            // we need to `toString` it because it's an `ObjectId`
            author: { id: comment.author.toString() }
          }

          notifier.notify(eventName)
            .to(reply.author.email)
            .withData( { reply: r, comment: c, url: topicUrl} )
            .send(function (err, data) {
              if (err) {
                log('Error when sending notification for event %s: %j', eventName, err);
                return fn(err);
              }

              log('Delivering reply %s', doc.id);
              return fn(null, doc);
            })
        } else {
          return fn(null, doc);
        }
      });
    });
  });
};

/**
 * Edit a reply
 *
 * @param {Object} comment to attach reply
 * @param {Object} reply object with params
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @api public
 */

exports.editReply = function editReply(comment, reply, fn) {
  log('Looking for comment %s to reply with %s', comment.id, reply.id);

  reply.editedAt = Date.now();

  Comment.update(
    { _id: comment.id, 'replies._id': reply.id },
    { $set: { 'replies.$.text': reply.text, 'replies.$.editedAt': reply.editedAt } },
    function (err) {
      if (err) {
        log('Found error %j', err);
        return fn(err);
      }

      log('Delivering reply %s', reply.id);
      fn(null, reply);
  });
};

/**
 * Upvote comment
 *
 * @param {String} id
 * @param {User|ObjectId|String} user
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comment' list items found or `undefined`
 * @api public
 */

exports.upvote = function upvote(id, user, fn) {
  Comment.findById(id, function(err, comment) {
    if (err) return log('Found error %s', err), fn(err);

    User.populate(comment, { path: 'author' }, function (err, comment) {
      if (err) return log('Found error %s', err), fn(err);

      if (comment.author.id == user.id) {
        log('Author %s tried to vote their own comment %s', user.id, comment.id);
        return fn(t('comments.score.not-allowed'), comment);
      }

      log('Upvoting comment %s', comment.id);
      comment.vote(user, 'positive', function(err) {
        if (err) return log('Found error %s', err), fn(err);

        log('Delivering comment %s', comment.id);
        fn(null, comment);
      });
    })
  });
};

/**
 * Downvote comment
 *
 * @param {String} id
 * @param {User|ObjectId|String} user
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @api public
 */

exports.downvote = function downvote(id, user, fn) {
  Comment.findById(id, function(err, comment) {
    if (err) return log('Found error %s', err), fn(err);

    User.populate(comment, { path: 'author' }, function (err, comment) {
      if (err) return log('Found error %s', err), fn(err);

      if (comment.author.id == user.id) {
        log('Author %s tried to vote their own comment %s', user.id, comment.id);
        return fn(t('comments.score.not-allowed'), comment);
      }

      log('Downvoting comment %s', comment.id);
      comment.vote(user, 'negative', function(err) {
        if (err) return log('Found error %s', err), fn(err);

        log('Delivering comment %s', comment.id);
        fn(null, comment);
      });
    });
  });
};

/**
 * Remove votation positive/negative from some comment.
 *
 * @param {String} id Comment id.
 * @param {Function} fn Callback function
 * @param {User} user The voe user
 */
exports.unvote = function (id, user, fn) {
  Comment.findById(id, function (err, comment) {

    if (err) {
      log('Found error %s', err);
      return fn(err);
    }

    User.populate(comment, { path: 'author' }, function (err, comment) {
      if (err) {
        log('Found error %s', err);
        return fn(err);
      }

      log('Remove vote from comment %s',comment.id);

      comment.unvote(user, function (err) {
        if (err) {
          log('Found error %s', err);
          return fn(err);
        }
        log('Delivering unvoted comment %s', comment.id);
        fn(null, comment);
      });
    });
  });
};

/**
 * Flag comment as spam
 *
 * @param {String} id
 * @param {User|ObjectId|String} user
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comment' list items found or `undefined`
 * @api public
 */

exports.flag = function flag(id, user, fn) {
  Comment.findById(id, function(err, comment) {
    if (err) return log('Found error %s', err), fn(err);

    log('Upvoting comment %s', comment.id);
    comment.flag(user, 'spam', function(err) {
      if (err) return log('Found error %s', err), fn(err);

      log('Delivering comment %s', comment.id);
      fn(null, comment);
    });
  });
};

/**
 * Unflag comment as spam
 *
 * @param {String} id
 * @param {User|ObjectId|String} user
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @api public
 */

exports.unflag = function unflag(id, user, fn) {
  Comment.findById(id, function(err, comment) {
    if (err) return log('Found error %s', err), fn(err);

    log('Downvoting comment %s', comment.id);
    comment.unflag(user, function(err) {
      if (err) return log('Found error %s', err), fn(err);

      log('Delivering comment %s', comment.id);
      fn(null, comment);
    });
  });
};

/**
 * Edit comment
 *
 * @param {String} id
 * @param {User|ObjectId|String} user
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 * @api public
 */

exports.edit = function edit(comment, fn) {
  log('Updating comment %s', comment.id);

  comment.save(function (err, comment) {
    if (!err) return log('Updated comment %s', comment.id), fn(null, comment);
    return log('Found error %s', err), fn(err);
  });

  return this;
};

/**
 * Remove comment
 *
 * @param {String} id
 * @param {User|ObjectId|String} user
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 * @api public
 */

exports.remove = function remove(comment, fn) {
  comment.remove(function(err) {
    if (err) return log('Found error %s', err), fn(err);

    log('Comment %s removed', comment.id);
    fn(null);
  });
};

/**
 * Search comment ratings
 *
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'ratings', total rated comments or `undefined`
 * @return {Module} `topic` module
 * @api public
 */

exports.ratings = function ratings(fn) {
  log('Counting total rated comments');

  Comment
    .aggregate(
      { $unwind : "$votes" },
      { $group: { _id: "#votes", total: { $sum: 1 } } },
      function (err, res) {
        if (err) {
          log('Found error: %j', err);
          return fn(err);
        }

        if (!res[0]) return fn(null, 0);

        var rated = res[0].total;

        log('Found %d rated comments', rated);
        fn(null, rated);
      }
    );

  return this;
};

/**
 * Total replies
 *
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'replies', total comment replies or `undefined`
 * @return {Module} `topic` module
 * @api public
 */

exports.totalReplies = function totalReplies(fn) {
  log('Counting total comment replies');

  Comment
    .aggregate(
      { $unwind : "$replies" },
      { $group: { _id: "#replies", total: { $sum: 1 } } },
      function (err, res) {
        if (err) {
          log('Found error: %j', err);
          return fn(err);
        }

        if (!res[0]) return fn(null, 0);

        var replies = res[0].total;

        log('Found %d comment replies', replies);
        fn(null, replies);
      }
    )

  return this;
};<commit_msg>Fix usage of ObjectIds for notification payload<commit_after>/**
 * Extend module's NODE_PATH
 * HACK: temporary solution
 */

require('node-path')(module);

/**
 * Module dependencies.
 */

var mongoose = require('mongoose');
var Comment = mongoose.model('Comment');
var utils = require('lib/utils');
var pluck = utils.pluck;
var config = require('lib/config');
var t = require('t-component');
var log = require('debug')('democracyos:db-api:comment');
var config = require('lib/config');
var utils = require('lib/utils');
var notifier = require('lib/notifications').notifier;
var User = require('lib/models').User;

/**
 * Get all comments
 *
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @return {Module} `comment` module
 * @api public
 */

exports.all = function all(fn) {
  log('Looking for all comments.')

  Comment.find(function (err, comments) {
    if (err) {
      log('Found error %j', err);
      return fn(err);
    };

    log('Delivering comments %j', pluck(comments, 'id'));
    fn(null, comments);
  });

  return this;
};

/**
 * Create comment for `proposal` by `author`
 * with `text`
 *
 * @param {String} proposal to submit comment
 * @param {Object} comment comment vars like `text` and `author`
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @api public
 */

exports.create = function create(comment, fn) {
  log('Creating new comment %j for %s %s', comment.text, comment.context, comment.reference);

  var comment = new Comment(comment);

  comment.save(function (err) {
    if (err) {
      log('Found error %s', err);
      return fn(err);
    };
    User.populate(comment, { path: 'author' }, function(err) {
      if (err) {
        log('Found error %s', err)
        return fn(err);
      };

      log('Delivering comment %j', comment.id);
      fn(null, comment);
    });
  });
};

/**
 * Get comments for proposal
 *
 * @param {String} proposal to get comments from
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @api public
 */

exports.getFor = function getFor(query, paging, fn) {
  log('Looking for comments for %s %s', query.context, query.reference);

  paging = paging || { page: 0, limit: config.commentsPerPage, sort: 'score', exclude_user: null };

  Comment
  .find(query)
  .sort(paging.sort || 'score')
  .skip(paging.page * paging.limit)
  .limit(paging.limit)
  .exec(function(err, comments) {
    if (err) {
      log('Found error %j', err);
      return fn(err);
    };

    var opts = { path: 'author', select: 'id firstName lastName fullName email avatar' };

    User.populate(comments, opts, function(err, comments) {
      if (err) {
        log('Found error %j', err);
        return fn(err);
      };

      log('Delivering comments %j', pluck(comments, 'id'));
      fn(null, comments);
    });
  });
};

/**
 * Get replies for comment
 *
 * @param {String} id
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @api public
 */

exports.replies = function replies(id, fn) {
  log('Looking for replies for comment %s', id);

  Comment
  .findOne({ _id: id })
  .exec(function(err, comment) {
    if (err) {
      log('Found error %j', err);
      return fn(err);
    };

    var opts = { path: 'replies.author', select: 'id firstName lastName fullName email avatar' };

    User.populate(comment, opts, function(err, comment) {
      if (err) {
        log('Found error %j', err);
        return fn(err);
      };

      var replies = comment && comment.replies ? comment.replies : [];

      log('Delivering replies %j', pluck(replies, 'id'));
      fn(null, replies);
    });
  });
};

/**
 * Reply to comment
 *
 * @param {String} commentId to attach reply
 * @param {Object} reply object with params
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @api public
 */

exports.reply = function reply(commentId, reply, fn) {
  log('Looking for comment %s to reply with %j', commentId, reply);

  Comment.findById(commentId, function(err, comment) {
    if (err) {
      log('Found error %j', err);
      return fn(err);
    };

    log('Creating reply %j for comment %j', reply, comment);
    var doc = comment.replies.create(reply);
    comment.replies.push(doc);

    comment.save(function(err, saved) {

      if (err) {
        log('Found error %j', err);
        return fn(err);
      };

      var opts = { path: 'replies.author', select: 'id firstName lastName fullName email avatar' };

      User.populate(comment, opts, function(err, comment) {
        if (err) {
          log('Found error %j', err);
          return fn(err);
        }

        if (comment.author != reply.author.id) {

          var eventName = 'comment-reply'

          var topicUrl = utils.buildUrl(config, { pathname: '/topic/' + comment.reference });

          var r = {
            id: doc.id,
            author: { id: reply.author.id },
            text: reply.text
          }

          var c = {
            id: comment.id,
            author: { id: comment.author }
          }

          notifier.notify(eventName)
            .to(reply.author.email)
            .withData( { reply: r, comment: c, url: topicUrl} )
            .send(function (err, data) {
              if (err) {
                log('Error when sending notification for event %s: %j', eventName, err);
                return fn(err);
              }

              log('Delivering reply %s', doc.id);
              return fn(null, doc);
            })
        } else {
          return fn(null, doc);
        }
      });
    });
  });
};

/**
 * Edit a reply
 *
 * @param {Object} comment to attach reply
 * @param {Object} reply object with params
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @api public
 */

exports.editReply = function editReply(comment, reply, fn) {
  log('Looking for comment %s to reply with %s', comment.id, reply.id);

  reply.editedAt = Date.now();

  Comment.update(
    { _id: comment.id, 'replies._id': reply.id },
    { $set: { 'replies.$.text': reply.text, 'replies.$.editedAt': reply.editedAt } },
    function (err) {
      if (err) {
        log('Found error %j', err);
        return fn(err);
      }

      log('Delivering reply %s', reply.id);
      fn(null, reply);
  });
};

/**
 * Upvote comment
 *
 * @param {String} id
 * @param {User|ObjectId|String} user
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comment' list items found or `undefined`
 * @api public
 */

exports.upvote = function upvote(id, user, fn) {
  Comment.findById(id, function(err, comment) {
    if (err) return log('Found error %s', err), fn(err);

    User.populate(comment, { path: 'author' }, function (err, comment) {
      if (err) return log('Found error %s', err), fn(err);

      if (comment.author.id == user.id) {
        log('Author %s tried to vote their own comment %s', user.id, comment.id);
        return fn(t('comments.score.not-allowed'), comment);
      }

      log('Upvoting comment %s', comment.id);
      comment.vote(user, 'positive', function(err) {
        if (err) return log('Found error %s', err), fn(err);

        log('Delivering comment %s', comment.id);
        fn(null, comment);
      });
    })
  });
};

/**
 * Downvote comment
 *
 * @param {String} id
 * @param {User|ObjectId|String} user
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @api public
 */

exports.downvote = function downvote(id, user, fn) {
  Comment.findById(id, function(err, comment) {
    if (err) return log('Found error %s', err), fn(err);

    User.populate(comment, { path: 'author' }, function (err, comment) {
      if (err) return log('Found error %s', err), fn(err);

      if (comment.author.id == user.id) {
        log('Author %s tried to vote their own comment %s', user.id, comment.id);
        return fn(t('comments.score.not-allowed'), comment);
      }

      log('Downvoting comment %s', comment.id);
      comment.vote(user, 'negative', function(err) {
        if (err) return log('Found error %s', err), fn(err);

        log('Delivering comment %s', comment.id);
        fn(null, comment);
      });
    });
  });
};

/**
 * Remove votation positive/negative from some comment.
 *
 * @param {String} id Comment id.
 * @param {Function} fn Callback function
 * @param {User} user The voe user
 */
exports.unvote = function (id, user, fn) {
  Comment.findById(id, function (err, comment) {

    if (err) {
      log('Found error %s', err);
      return fn(err);
    }

    User.populate(comment, { path: 'author' }, function (err, comment) {
      if (err) {
        log('Found error %s', err);
        return fn(err);
      }

      log('Remove vote from comment %s',comment.id);

      comment.unvote(user, function (err) {
        if (err) {
          log('Found error %s', err);
          return fn(err);
        }
        log('Delivering unvoted comment %s', comment.id);
        fn(null, comment);
      });
    });
  });
};

/**
 * Flag comment as spam
 *
 * @param {String} id
 * @param {User|ObjectId|String} user
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comment' list items found or `undefined`
 * @api public
 */

exports.flag = function flag(id, user, fn) {
  Comment.findById(id, function(err, comment) {
    if (err) return log('Found error %s', err), fn(err);

    log('Upvoting comment %s', comment.id);
    comment.flag(user, 'spam', function(err) {
      if (err) return log('Found error %s', err), fn(err);

      log('Delivering comment %s', comment.id);
      fn(null, comment);
    });
  });
};

/**
 * Unflag comment as spam
 *
 * @param {String} id
 * @param {User|ObjectId|String} user
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'comments' list items found or `undefined`
 * @api public
 */

exports.unflag = function unflag(id, user, fn) {
  Comment.findById(id, function(err, comment) {
    if (err) return log('Found error %s', err), fn(err);

    log('Downvoting comment %s', comment.id);
    comment.unflag(user, function(err) {
      if (err) return log('Found error %s', err), fn(err);

      log('Delivering comment %s', comment.id);
      fn(null, comment);
    });
  });
};

/**
 * Edit comment
 *
 * @param {String} id
 * @param {User|ObjectId|String} user
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 * @api public
 */

exports.edit = function edit(comment, fn) {
  log('Updating comment %s', comment.id);

  comment.save(function (err, comment) {
    if (!err) return log('Updated comment %s', comment.id), fn(null, comment);
    return log('Found error %s', err), fn(err);
  });

  return this;
};

/**
 * Remove comment
 *
 * @param {String} id
 * @param {User|ObjectId|String} user
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 * @api public
 */

exports.remove = function remove(comment, fn) {
  comment.remove(function(err) {
    if (err) return log('Found error %s', err), fn(err);

    log('Comment %s removed', comment.id);
    fn(null);
  });
};

/**
 * Search comment ratings
 *
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'ratings', total rated comments or `undefined`
 * @return {Module} `topic` module
 * @api public
 */

exports.ratings = function ratings(fn) {
  log('Counting total rated comments');

  Comment
    .aggregate(
      { $unwind : "$votes" },
      { $group: { _id: "#votes", total: { $sum: 1 } } },
      function (err, res) {
        if (err) {
          log('Found error: %j', err);
          return fn(err);
        }

        if (!res[0]) return fn(null, 0);

        var rated = res[0].total;

        log('Found %d rated comments', rated);
        fn(null, rated);
      }
    );

  return this;
};

/**
 * Total replies
 *
 * @param {Function} fn callback function
 *   - 'err' error found while process or `null`
 *   - 'replies', total comment replies or `undefined`
 * @return {Module} `topic` module
 * @api public
 */

exports.totalReplies = function totalReplies(fn) {
  log('Counting total comment replies');

  Comment
    .aggregate(
      { $unwind : "$replies" },
      { $group: { _id: "#replies", total: { $sum: 1 } } },
      function (err, res) {
        if (err) {
          log('Found error: %j', err);
          return fn(err);
        }

        if (!res[0]) return fn(null, 0);

        var replies = res[0].total;

        log('Found %d comment replies', replies);
        fn(null, replies);
      }
    )

  return this;
};<|endoftext|>