<commit_before>"use strict"

const title = document.getElementById("title")
const msg = document.getElementById("msg")
const msg2 = document.getElementById("msg2")
const form = document.getElementById("form")
const input = document.getElementById("input")

const onePrep = e => {
    e.preventDefault()
    if (input.value.trim() === "") {
        msg2.textContent = "that can't be empty."
        return
    }
    one(input.value)
}

const one = question => {
    input.value = ""
    msg.textContent = `what are some factors that affect '${question}'?`
    msg2.textContent = "they should also be yes/no, and separated with commas."
    form.onsubmit = twoPrep(question)
}

const twoPrep = question => e => {
    e.preventDefault()
    if (input.value.trim() === "") {
        msg2.textContent = "that can't be empty."
        return
    }
    const factors = input.value.split(",").map(x => x.trim())
    if (factors.find(x => x === "") !== undefined) {
        msg2.textContent = "you can't have an empty factor."
        return
    }
    if (factors.length > 7) {
        msg2.textContent = "you can't have more than 7 factors."
        return
    }
    two(question, factors)
}

const isGreen = x => x.className === "green"
const newRow = n => {
    const row = document.createElement("div")
    for (let i = 0; i < n; i++) {
        const el = document.createElement("span")
        el.className = "green"
        el.textContent = "y"
        el.onclick = () => {
            const ig = isGreen(el)
            el.className = ig ? "red" : "green"
            el.textContent = ig ? "n" : "y"
        }
        row.appendChild(el)
    }
    return row
}

const two = (question, factors) => {
    input.remove()
    msg.textContent = "enter some data."
    msg2.textContent = "click to add rows and switch between yes/no."

    const header = document.createElement("div")
    let el

    for (const x of factors) {
        el = document.createElement("span")
        el.textContent = x
        el.className = "blue"
        header.appendChild(el)
    }

    el = document.createElement("span")
    el.textContent = question
    el.className = "cyan"
    header.appendChild(el)

    form.appendChild(header)

    const footer = document.createElement("div")

    const addRowBtn = document.createElement("span")
    addRowBtn.innerHTML = "add datapoint"
    addRowBtn.className = "gray"
    addRowBtn.onclick = () => {
        form.insertBefore(newRow(factors.length + 1), footer)
    }

    const submitBtn = document.createElement("span")
    submitBtn.innerHTML = "submit data"
    submitBtn.className = "yellow"
    submitBtn.onclick = threePrep

    footer.appendChild(addRowBtn)
    footer.appendChild(submitBtn)
    form.appendChild(footer)
}

const toData = x => isGreen(x) ? 1 : 0
const threePrep = e => {
    e.preventDefault()
    // a gross hack
    // it would be better to use one of those fancy front-end libraries with
    // two-way binding and whatnot
    const fc = form.children
    const max = fc.length - 1
    const data = []
    const results = []
    // skip the header and footer
    for (let i = 1; i < max; i++) {
        const r = fc[i].children
        const rMax = r.length - 1
        const ary = []
        for (let j = 0; j < rMax; j++) {
            ary.push(toData(r[j]))
        }
        data.push(ary)
        results.push(toData(r[rMax]))
    }
    console.log({data, results})
    fetch("/train", {
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify({data, results})
    })
    .then(x => x.json())
    .then(x => console.log(x))
}

title.textContent = "machine learning™"
msg.textContent = "what is your question for today?"
msg2.textContent = "it should be a yes/no question."
input.focus()
form.onsubmit = onePrep
form.style.display = "block"
<commit_msg>change addRow<commit_after>"use strict"

const title = document.getElementById("title")
const msg = document.getElementById("msg")
const msg2 = document.getElementById("msg2")
const form = document.getElementById("form")
const input = document.getElementById("input")

const onePrep = e => {
    e.preventDefault()
    if (input.value.trim() === "") {
        msg2.textContent = "that can't be empty."
        return
    }
    one(input.value)
}

const one = question => {
    input.value = ""
    msg.textContent = `what are some factors that affect '${question}'?`
    msg2.textContent = "they should also be yes/no, and separated with commas."
    form.onsubmit = twoPrep(question)
}

const twoPrep = question => e => {
    e.preventDefault()
    if (input.value.trim() === "") {
        msg2.textContent = "that can't be empty."
        return
    }
    const factors = input.value.split(",").map(x => x.trim())
    if (factors.find(x => x === "") !== undefined) {
        msg2.textContent = "you can't have an empty factor."
        return
    }
    if (factors.length > 7) {
        msg2.textContent = "you can't have more than 7 factors."
        return
    }
    two(question, factors)
}

const isGreen = x => x.className === "green"
const addRow = n => isCalculated => () => {
    const row = document.createElement("div")
    for (let i = 0; i < n; i++) {
        const el = document.createElement("span")
        if (isCalculated && i === n - 1) {
            el.className = "gray"
            el.textContent = "?"
            return
        }
        el.className = "green"
        el.textContent = "y"
        el.onclick = () => {
            const ig = isGreen(el)
            el.className = ig ? "red" : "green"
            el.textContent = ig ? "n" : "y"
        }
        row.appendChild(el)
    }
    form.insertBefore(row, form.lastChild)
}

const two = (question, factors) => {
    input.remove()
    msg.textContent = "enter some data."
    msg2.textContent = "click to add rows and switch between yes/no."

    const header = document.createElement("div")
    let el

    for (const x of factors) {
        el = document.createElement("span")
        el.textContent = x
        el.className = "blue"
        header.appendChild(el)
    }

    el = document.createElement("span")
    el.textContent = question
    el.className = "cyan"
    header.appendChild(el)

    form.appendChild(header)

    const footer = document.createElement("div")

    const addRowBtn = document.createElement("span")
    addRowBtn.innerHTML = "add datapoint"
    addRowBtn.className = "gray"
    const thisAddRow = addRow(factors.length + 1)
    addRowBtn.onclick = thisAddRow(false)

    const submitBtn = document.createElement("span")
    submitBtn.innerHTML = "submit data"
    submitBtn.className = "yellow"
    submitBtn.onclick = threePrep

    footer.appendChild(addRowBtn)
    footer.appendChild(submitBtn)
    form.appendChild(footer)
}

const toData = x => isGreen(x) ? 1 : 0
const threePrep = e => {
    e.preventDefault()
    // a gross hack
    // it would be better to use one of those fancy front-end libraries with
    // two-way binding and whatnot
    const fc = form.children
    const max = fc.length - 1
    const data = []
    const results = []
    // skip the header and footer
    for (let i = 1; i < max; i++) {
        const r = fc[i].children
        const rMax = r.length - 1
        const ary = []
        for (let j = 0; j < rMax; j++) {
            ary.push(toData(r[j]))
        }
        data.push(ary)
        results.push(toData(r[rMax]))
    }
    console.log({data, results})
    fetch("/train", {
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify({data, results})
    })
    .then(x => x.json())
    .then(x => console.log(x))
}

title.textContent = "machine learning™"
msg.textContent = "what is your question for today?"
msg2.textContent = "it should be a yes/no question."
input.focus()
form.onsubmit = onePrep
form.style.display = "block"
<|endoftext|>


<commit_before>"use strict"

const row = require("./row")

const title = document.getElementById("title")
const msg = document.getElementById("msg")
const msg2 = document.getElementById("msg2")
const form = document.getElementById("form")
const input = document.getElementById("input")

let question = ""
let factors = []

const one = e => {
    e.preventDefault()
    if (input.value.trim() === "") {
        msg2.textContent = "that can't be empty."
        return
    }
    question = input.value
    input.value = ""
    msg.textContent = `what are some factors that affect '${question}'?`
    msg2.textContent = "they should also be yes/no, and separated with commas."
    form.onsubmit = two
}

const two = e => {
    e.preventDefault()
    if (input.value.trim() === "") {
        msg2.textContent = "that can't be empty."
        return
    }
    factors = input.value.split(",").map(x => x.trim())
    if (factors.find(x => x === "") !== undefined) {
        msg2.textContent = "you can't have an empty factor."
        return
    }
    if (factors.length > 7) {
        msg2.textContent = "you can't have more than 7 factors."
        return
    }
    input.remove()
    const header = row(factors.length + 1)
    let i
    for (i = 0; i < factors.length; i++) {
        header.children[i].textContent = factors[i]
    }
    header.children[i].textContent = question
    form.appendChild(header)
}

title.textContent = "machine learning™"
msg.textContent = "what is your question for today?"
msg2.textContent = "it should be a yes/no question."
input.focus()
form.onsubmit = one
form.style.display = "block"
<commit_msg>don't use global<commit_after>"use strict"

const row = require("./row")

const title = document.getElementById("title")
const msg = document.getElementById("msg")
const msg2 = document.getElementById("msg2")
const form = document.getElementById("form")
const input = document.getElementById("input")

const onePrep = () => e => {
    e.preventDefault()
    if (input.value.trim() === "") {
        msg2.textContent = "that can't be empty."
        return
    }
    one(input.value)
}

const one = question => {
    input.value = ""
    msg.textContent = `what are some factors that affect '${question}'?`
    msg2.textContent = "they should also be yes/no, and separated with commas."
    form.onsubmit = twoPrep(question)
}

const twoPrep = question => e => {
    e.preventDefault()
    if (input.value.trim() === "") {
        msg2.textContent = "that can't be empty."
        return
    }
    const factors = input.value.split(",").map(x => x.trim())
    if (factors.find(x => x === "") !== undefined) {
        msg2.textContent = "you can't have an empty factor."
        return
    }
    if (factors.length > 7) {
        msg2.textContent = "you can't have more than 7 factors."
        return
    }
    two(question, factors)
}

const two = (question, factors) => {
    input.remove()
    const header = row(factors.length + 1)
    let i
    for (i = 0; i < factors.length; i++) {
        header.children[i].textContent = factors[i]
    }
    header.children[i].textContent = question
    form.appendChild(header)
}

title.textContent = "machine learning™"
msg.textContent = "what is your question for today?"
msg2.textContent = "it should be a yes/no question."
input.focus()
form.onsubmit = onePrep()
form.style.display = "block"
<|endoftext|>


<commit_before>/**
 * @preserve
 *
 *                                     .,,,;;,'''..
 *                                 .'','...     ..',,,.
 *                               .,,,,,,',,',;;:;,.  .,l,
 *                              .,',.     ...     ,;,   :l.
 *                             ':;.    .'.:do;;.    .c   ol;'.
 *      ';;'                   ;.;    ', .dkl';,    .c   :; .'.',::,,'''.
 *     ',,;;;,.                ; .,'     .'''.    .'.   .d;''.''''.
 *    .oxddl;::,,.             ',  .'''.   .... .'.   ,:;..
 *     .'cOX0OOkdoc.            .,'.   .. .....     'lc.
 *    .:;,,::co0XOko'              ....''..'.'''''''.
 *    .dxk0KKdc:cdOXKl............. .. ..,c....
 *     .',lxOOxl:'':xkl,',......'....    ,'.
 *          .';:oo:...                        .
 *               .cd,    ╔═╗┌─┐┬─┐┬  ┬┌─┐┬─┐   .
 *                 .l;   ╚═╗├┤ ├┬┘└┐┌┘├┤ ├┬┘   '
 *                   'l. ╚═╝└─┘┴└─ └┘ └─┘┴└─  '.
 *                    .o.                   ...
 *                     .''''','.;:''.........
 *                          .'  .l
 *                         .:.   l'
 *                        .:.    .l.
 *                       .x:      :k;,.
 *                       cxlc;    cdc,,;;.
 *                      'l :..   .c  ,
 *                      o.
 *                     .,
 *
 *             ╦ ╦┬ ┬┌┐ ┬─┐┬┌┬┐  ╔═╗┌┐  ┬┌─┐┌─┐┌┬┐┌─┐
 *             ╠═╣└┬┘├┴┐├┬┘│ ││  ║ ║├┴┐ │├┤ │   │ └─┐
 *             ╩ ╩ ┴ └─┘┴└─┴─┴┘  ╚═╝└─┘└┘└─┘└─┘ ┴ └─┘
 *
 * Created by Valentin on 10/22/14.
 *
 * Copyright (c) 2015 Valentin Heun
 *
 * All ascii characters above must be included in any redistribution.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

// Load socket.io.js synchronous so that it is available by the time the rest of the code is executed.
var xhr = new XMLHttpRequest();
xhr.open('GET', "/socket.io/socket.io.js", false);
xhr.send();
var objIOScript = document.createElement('script');
objIOScript.type = "text/javascript";
objIOScript.text = xhr.responseText;
document.getElementsByTagName('head')[0].appendChild(objIOScript);

// function for resizing the windows.
var objectExp = {};

window.addEventListener("message", function (msg) {
    parent.postMessage(JSON.stringify(
        {
            "pos": JSON.parse(msg.data).pos,
            "obj": JSON.parse(msg.data).obj,
            "height": document.body.scrollHeight,
            "width": document.body.scrollWidth
        }
        )
        // this needs to contain the final interface source
        , "*");
    objectExp.pos = JSON.parse(msg.data).pos;
    objectExp.obj = JSON.parse(msg.data).obj;

}, false);

// adding css styles nessasary for acurate 3D transformations.
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = 'body, html{ height: 100%; margin:0; padding:0;}';
document.getElementsByTagName('head')[0].appendChild(style);

function HybridObject() {
    if (typeof io !== "undefined") {
        this.object = io.connect();

        this.write = function (IO, value, mode) {
            if (!mode) mode = 'f';
            this.object.emit('object', JSON.stringify({pos: IO, obj: objectExp.obj, value: value, mode: mode}));
        };

        this.readRequest = function (IO) {
            this.object.emit('/object/value', JSON.stringify({pos: IO, obj: objectExp.obj}));
        };

        this.read = function (IO, data) {
            if (data.pos === IO) {
                return data.value;
            } else {
                return undefined;
            }
        };
        console.log("socket.io is loaded");
    }
    else {
        this.object = {
            on: function (x, cb) {
            }
        };
        this.write = function (IO, value, mode) {
        };
        this.read = function (IO, data) {
             return undefined;
        };
        this.readRequest = function (IO) {
        };
        console.log("socket.io is not working. This is normal when you work offline.");
    }
}<commit_msg>Added some more error handling. Only add socket.io.js if it could be fetched successfully<commit_after>/**
 * @preserve
 *
 *                                     .,,,;;,'''..
 *                                 .'','...     ..',,,.
 *                               .,,,,,,',,',;;:;,.  .,l,
 *                              .,',.     ...     ,;,   :l.
 *                             ':;.    .'.:do;;.    .c   ol;'.
 *      ';;'                   ;.;    ', .dkl';,    .c   :; .'.',::,,'''.
 *     ',,;;;,.                ; .,'     .'''.    .'.   .d;''.''''.
 *    .oxddl;::,,.             ',  .'''.   .... .'.   ,:;..
 *     .'cOX0OOkdoc.            .,'.   .. .....     'lc.
 *    .:;,,::co0XOko'              ....''..'.'''''''.
 *    .dxk0KKdc:cdOXKl............. .. ..,c....
 *     .',lxOOxl:'':xkl,',......'....    ,'.
 *          .';:oo:...                        .
 *               .cd,    ╔═╗┌─┐┬─┐┬  ┬┌─┐┬─┐   .
 *                 .l;   ╚═╗├┤ ├┬┘└┐┌┘├┤ ├┬┘   '
 *                   'l. ╚═╝└─┘┴└─ └┘ └─┘┴└─  '.
 *                    .o.                   ...
 *                     .''''','.;:''.........
 *                          .'  .l
 *                         .:.   l'
 *                        .:.    .l.
 *                       .x:      :k;,.
 *                       cxlc;    cdc,,;;.
 *                      'l :..   .c  ,
 *                      o.
 *                     .,
 *
 *             ╦ ╦┬ ┬┌┐ ┬─┐┬┌┬┐  ╔═╗┌┐  ┬┌─┐┌─┐┌┬┐┌─┐
 *             ╠═╣└┬┘├┴┐├┬┘│ ││  ║ ║├┴┐ │├┤ │   │ └─┐
 *             ╩ ╩ ┴ └─┘┴└─┴─┴┘  ╚═╝└─┘└┘└─┘└─┘ ┴ └─┘
 *
 * Created by Valentin on 10/22/14.
 *
 * Copyright (c) 2015 Valentin Heun
 *
 * All ascii characters above must be included in any redistribution.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

// Load socket.io.js synchronous so that it is available by the time the rest of the code is executed.
var xhr = new XMLHttpRequest();
xhr.open('GET', "/socket.io/socket.io.js", false);
xhr.send();

//Only add script if fetch was successful
if (xhr.status == 200) {
    var objIOScript = document.createElement('script');
    objIOScript.type = "text/javascript";
    objIOScript.text = xhr.responseText;
    document.getElementsByTagName('head')[0].appendChild(objIOScript);
} else {
    console.log("Error XMLHttpRequest HTTP status: " + xhr.status);
}

// function for resizing the windows.
var objectExp = {};

window.addEventListener("message", function (msg) {
    parent.postMessage(JSON.stringify(
        {
            "pos": JSON.parse(msg.data).pos,
            "obj": JSON.parse(msg.data).obj,
            "height": document.body.scrollHeight,
            "width": document.body.scrollWidth
        }
        )
        // this needs to contain the final interface source
        , "*");
    objectExp.pos = JSON.parse(msg.data).pos;
    objectExp.obj = JSON.parse(msg.data).obj;

}, false);

// adding css styles nessasary for acurate 3D transformations.
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = 'body, html{ height: 100%; margin:0; padding:0;}';
document.getElementsByTagName('head')[0].appendChild(style);

function HybridObject() {
    if (typeof io !== "undefined") {
        this.object = io.connect();

        this.write = function (IO, value, mode) {
            if (!mode) mode = 'f';
            this.object.emit('object', JSON.stringify({pos: IO, obj: objectExp.obj, value: value, mode: mode}));
        };

        this.readRequest = function (IO) {
            this.object.emit('/object/value', JSON.stringify({pos: IO, obj: objectExp.obj}));
        };

        this.read = function (IO, data) {
            if (data.pos === IO) {
                return data.value;
            } else {
                return undefined;
            }
        };
        console.log("socket.io is loaded");
    }
    else {
        this.object = {
            on: function (x, cb) {
            }
        };
        this.write = function (IO, value, mode) {
        };
        this.read = function (IO, data) {
             return undefined;
        };
        this.readRequest = function (IO) {
        };
        console.log("socket.io is not working. This is normal when you work offline.");
    }
}<|endoftext|>


<commit_before>(function(global) {
  var RS = RemoteStorage;

  /**
   * Class: RemoteStorage.WireClient
   *
   * WireClient Interface
   * --------------------
   *
   * This file exposes a get/put/delete interface on top of XMLHttpRequest.
   * It requires to be configured with parameters about the remotestorage server to
   * connect to.
   * Each instance of WireClient is always associated with a single remotestorage
   * server and access token.
   *
   * Usually the WireClient instance can be accessed via `remoteStorage.remote`.
   *
   * This is the get/put/delete interface:
   *
   *   - #get() takes a path and optionally a ifNoneMatch option carrying a version
   *     string to check. It returns a promise that will be fulfilled with the HTTP
   *     response status, the response body, the MIME type as returned in the
   *     'Content-Type' header and the current revision, as returned in the 'ETag'
   *     header.
   *   - #put() takes a path, the request body and a content type string. It also
   *     accepts the ifMatch and ifNoneMatch options, that map to the If-Match and
   *     If-None-Match headers respectively. See the remotestorage-01 specification
   *     for details on handling these headers. It returns a promise, fulfilled with
   *     the same values as the one for #get().
   *   - #delete() takes a path and the ifMatch option as well. It returns a promise
   *     fulfilled with the same values as the one for #get().
   *
   * In addition to this, the WireClient has some compatibility features to work with
   * remotestorage 2012.04 compatible storages. For example it will cache revisions
   * from folder listings in-memory and return them accordingly as the "revision"
   * parameter in response to #get() requests. Similarly it will return 404 when it
   * receives an empty folder listing, to mimic remotestorage-01 behavior. Note
   * that it is not always possible to know the revision beforehand, hence it may
   * be undefined at times (especially for caching-roots).
   */

  var hasLocalStorage;
  var SETTINGS_KEY = "remotestorage:wireclient";

  var API_2012 = 1, API_00 = 2, API_01 = 3, API_02 = 4, API_HEAD = 5;

  var STORAGE_APIS = {
    'draft-dejong-remotestorage-00': API_00,
    'draft-dejong-remotestorage-01': API_01,
    'draft-dejong-remotestorage-02': API_02,
    'https://www.w3.org/community/rww/wiki/read-write-web-00#simple': API_2012
  };

  var isArrayBufferView;

  if (typeof(ArrayBufferView) === 'function') {
    isArrayBufferView = function(object) { return object && (object instanceof ArrayBufferView); };
  } else {
    var arrayBufferViews = [
      Int8Array, Uint8Array, Int16Array, Uint16Array,
      Int32Array, Uint32Array, Float32Array, Float64Array
    ];
    isArrayBufferView = function(object) {
      for (var i=0;i<8;i++) {
        if (object instanceof arrayBufferViews[i]) {
          return true;
        }
      }
      return false;
    };
  }

  function readBinaryData(content, mimeType, callback) {
    var blob = new Blob([content], { type: mimeType });
    var reader = new FileReader();
    reader.addEventListener("loadend", function() {
      callback(reader.result); // reader.result contains the contents of blob as a typed array
    });
    reader.readAsArrayBuffer(blob);
  }

  function cleanPath(path) {
    return path.replace(/\/+/g, '/').split('/').map(encodeURIComponent).join('/');
  }

  function isFolder(path) {
    return (path.substr(-1) === '/');
  }

  function isFolderDescription(body) {
    return ((body['@context'] === 'http://remotestorage.io/spec/folder-description')
             && (typeof(body['items']) === 'object'));
  }

  var onErrorCb;

  /**
   * Class : RemoteStorage.WireClient
   **/
  RS.WireClient = function(rs) {
    this.connected = false;
    /**
     * Event: change
     *   never fired for some reason
     *
     * Event: connected
     *   fired when the wireclient connect method realizes that it is
     *   in posession of a token and a href
     **/
    RS.eventHandling(this, 'change', 'connected', 'wire-busy', 'wire-done');

    onErrorCb = function(error){
      if (error instanceof RemoteStorage.Unauthorized) {
        this.configure(undefined, undefined, undefined, null);
      }
    }.bind(this);
    rs.on('error', onErrorCb);
    if (hasLocalStorage) {
      var settings;
      try { settings = JSON.parse(localStorage[SETTINGS_KEY]); } catch(e) {}
      if (settings) {
        setTimeout(function() {
          this.configure(settings.userAddress, settings.href, settings.storageApi, settings.token);
        }.bind(this), 0);
      }
    }

    this._revisionCache = {};

    if (this.connected) {
      setTimeout(this._emit.bind(this), 0, 'connected');
    }
  };

  RS.WireClient.REQUEST_TIMEOUT = 30000;

  RS.WireClient.prototype = {
    /**
     * Property: token
     *
     * Holds the bearer token of this WireClient, as obtained in the OAuth dance
     *
     * Example:
     *   (start code)
     *
     *   remoteStorage.remote.token
     *   // -> 'DEADBEEF01=='
     */

    /**
     * Property: href
     *
     * Holds the server's base URL, as obtained in the Webfinger discovery
     *
     * Example:
     *   (start code)
     *
     *   remoteStorage.remote.href
     *   // -> 'https://storage.example.com/users/jblogg/'
     */

    /**
     * Property: storageApi
     *
     * Holds the spec version the server claims to be compatible with
     *
     * Example:
     *   (start code)
     *
     *   remoteStorage.remote.storageApi
     *   // -> 'draft-dejong-remotestorage-01'
     */

    _request: function(method, uri, token, headers, body, getEtag, fakeRevision) {
      if ((method === 'PUT' || method === 'DELETE') && uri[uri.length - 1] === '/') {
        throw "Don't " + method + " on directories!";
      }

      var promise = promising();
      var revision;
      var reqType;
      var self = this;

      headers['Authorization'] = 'Bearer ' + token;

      this._emit('wire-busy', {
        method: method,
        isFolder: isDir(uri)
      });

      RS.WireClient.request(method, uri, {
        body: body,
        headers: headers
      }, function(error, response) {
        if (error) {
          self._emit('wire-done', {
            method: method,
            isFolder: isDir(uri),
            success: false
          });
          promise.reject(error);
        } else {
          self._emit('wire-done', {
            method: method,
            isFolder: isDir(uri),
            success: true
          });
          if ([401, 403, 404, 412].indexOf(response.status) >= 0) {
            promise.fulfill(response.status);
          } else if ([201, 204, 304].indexOf(response.status) >= 0 ||
                     (response.status === 200 && method !== 'GET')) {
            revision = response.getResponseHeader('ETag');
            promise.fulfill(response.status, undefined, undefined, revision);
          } else {
            var mimeType = response.getResponseHeader('Content-Type');
            var body;
            if (getEtag) {
              revision = response.getResponseHeader('ETag');
            } else {
              revision = response.status === 200 ? fakeRevision : undefined;
            }

            if ((! mimeType) || mimeType.match(/charset=binary/)) {
              RS.WireClient.readBinaryData(response.response, mimeType, function(result) {
                promise.fulfill(response.status, result, mimeType, revision);
              });
            } else {
              if (mimeType && mimeType.match(/^application\/json/)) {
                body = JSON.parse(response.responseText);
              } else {
                body = response.responseText;
              }
              promise.fulfill(response.status, body, mimeType, revision);
            }
          }
        }
      });
      return promise;
    },

    configure: function(userAddress, href, storageApi, token) {
      if (typeof(userAddress) !== 'undefined') {
        this.userAddress = userAddress;
      }
      if (typeof(href) !== 'undefined') {
        this.href = href;
      }
      if (typeof(storageApi) !== 'undefined') {
        this.storageApi = storageApi;
      }
      if (typeof(token) !== 'undefined') {
        this.token = token;
      }
      if (typeof(this.storageApi) !== 'undefined') {
        this._storageApi = STORAGE_APIS[this.storageApi] || API_HEAD;
        this.supportsRevs = this._storageApi >= API_00;
      }
      if (this.href && this.token) {
        this.connected = true;
        this._emit('connected');
      } else {
        this.connected = false;
      }
      if (hasLocalStorage) {
        localStorage[SETTINGS_KEY] = JSON.stringify({
          userAddress: this.userAddress,
          href: this.href,
          token: this.token,
          storageApi: this.storageApi
        });
      }
      RS.WireClient.configureHooks.forEach(function(hook) {
        hook.call(this);
      }.bind(this));
    },

    get: function(path, options) {
      if (!this.connected) {
        throw new Error("not connected (path: " + path + ")");
      }
      if (!options) { options = {}; }
      var headers = {};
      if (this.supportsRevs) {
        if (options.ifNoneMatch) {
          headers['If-None-Match'] = options.ifNoneMatch;
        }
      } else if (options.ifNoneMatch) {
        var oldRev = this._revisionCache[path];
      }
      var promise = this._request('GET', this.href + cleanPath(path), this.token, headers,
                            undefined, this.supportsRevs, this._revisionCache[path]);
      if (!isFolder(path)) {
        return promise;
      } else {
        return promise.then(function(status, body, contentType, revision) {
          var listing = {};

          // New folder listing received
          if (status === 200 && typeof(body) === 'object') {
            // Empty folder listing of any spec
            if (Object.keys(body).length === 0) {
              status = 404;
            }
            // >= 02 spec
            else if (isFolderDescription(body)) {
              for (var item in body.items) {
                this._revisionCache[path + item] = body.items[item].ETag;
              }
              listing = body.items;
            }
            // < 02 spec
            else {
              Object.keys(body).forEach(function(key){
                this._revisionCache[path + key] = body[key];
                listing[key] = {"ETag": body[key]};
              }.bind(this));
            }
            return promising().fulfill(status, listing, contentType, revision);
          }
          // No folder listing received
          else if (status === 404) {
            return promising().fulfill(404);
          }
          // Cached folder listing received
          else if (status === 304) {
            return promising().fulfill(status, body, contentType, revision);
          }
          // Faulty folder listing received
          else {
            var error = new Error("Received faulty folder response for: "+path);
            return promising().reject(error);
          }
        }.bind(this));
      }
    },

    put: function(path, body, contentType, options) {
      if (!this.connected) {
        throw new Error("not connected (path: " + path + ")");
      }
      if (!options) { options = {}; }
      if (!contentType.match(/charset=/)) {
        contentType += '; charset=' + ((body instanceof ArrayBuffer || isArrayBufferView(body)) ? 'binary' : 'utf-8');
      }
      var headers = { 'Content-Type': contentType };
      if (this.supportsRevs) {
        if (options.ifMatch) {
          headers['If-Match'] = options.ifMatch;
        }
        if (options.ifNoneMatch) {
          headers['If-None-Match'] = options.ifNoneMatch;
        }
      }
      return this._request('PUT', this.href + cleanPath(path), this.token,
                     headers, body, this.supportsRevs);
    },

    'delete': function(path, options) {
      if (!this.connected) {
        throw new Error("not connected (path: " + path + ")");
      }
      if (!options) { options = {}; }
      var headers = {};
      if (this.supportsRevs) {
        if (options.ifMatch) {
          headers['If-Match'] = options.ifMatch;
        }
      }
      return this._request('DELETE', this.href + cleanPath(path), this.token,
                     headers,
                     undefined, this.supportsRevs);
    }
  };

  // Shared cleanPath used by Dropbox
  RS.WireClient.cleanPath = cleanPath;

  // Shared isArrayBufferView used by WireClient and Dropbox
  RS.WireClient.isArrayBufferView = isArrayBufferView;

  RS.WireClient.readBinaryData = readBinaryData;

  // Shared request function used by WireClient, GoogleDrive and Dropbox.
  RS.WireClient.request = function(method, url, options, callback) {
    RemoteStorage.log(method, url);

    callback = callback.bind(this);

    var timedOut = false;

    var timer = setTimeout(function() {
      timedOut = true;
      callback('timeout');
    }, RS.WireClient.REQUEST_TIMEOUT);

    var xhr = new XMLHttpRequest();
    xhr.open(method, url, true);

    if (options.responseType) {
      xhr.responseType = options.responseType;
    }
    if (options.headers) {
      for (var key in options.headers) {
        xhr.setRequestHeader(key, options.headers[key]);
      }
    }

    xhr.onload = function() {
      if (timedOut) { return; }
      clearTimeout(timer);
      callback(null, xhr);
    };

    xhr.onerror = function(error) {
      if (timedOut) { return; }
      clearTimeout(timer);
      callback(error);
    };

    var body = options.body;

    if (typeof(body) === 'object') {
      if (isArrayBufferView(body)) {
        /* alright. */
        //FIXME empty block
      }
      else if (body instanceof ArrayBuffer) {
        body = new Uint8Array(body);
      } else {
        body = JSON.stringify(body);
      }
    }
    xhr.send(body);
  };

  Object.defineProperty(RemoteStorage.WireClient.prototype, 'storageType', {
    get: function() {
      if (this.storageApi) {
        var spec = this.storageApi.match(/draft-dejong-(remotestorage-\d\d)/);
        return spec ? spec[1] : '2012.04';
      }
    }
  });

  RS.WireClient.configureHooks = [];

  RS.WireClient._rs_init = function(remoteStorage) {
    hasLocalStorage = remoteStorage.localStorageAvailable();
    remoteStorage.remote = new RS.WireClient(remoteStorage);
  };

  RS.WireClient._rs_supported = function() {
    return !! global.XMLHttpRequest;
  };

  RS.WireClient._rs_cleanup = function(remoteStorage){
    if (hasLocalStorage){
      delete localStorage[SETTINGS_KEY];
    }
    remoteStorage.removeEventListener('error', onErrorCb);
  };

})(typeof(window) !== 'undefined' ? window : global);
<commit_msg>Fix method call isDir -> isFolder<commit_after>(function(global) {
  var RS = RemoteStorage;

  /**
   * Class: RemoteStorage.WireClient
   *
   * WireClient Interface
   * --------------------
   *
   * This file exposes a get/put/delete interface on top of XMLHttpRequest.
   * It requires to be configured with parameters about the remotestorage server to
   * connect to.
   * Each instance of WireClient is always associated with a single remotestorage
   * server and access token.
   *
   * Usually the WireClient instance can be accessed via `remoteStorage.remote`.
   *
   * This is the get/put/delete interface:
   *
   *   - #get() takes a path and optionally a ifNoneMatch option carrying a version
   *     string to check. It returns a promise that will be fulfilled with the HTTP
   *     response status, the response body, the MIME type as returned in the
   *     'Content-Type' header and the current revision, as returned in the 'ETag'
   *     header.
   *   - #put() takes a path, the request body and a content type string. It also
   *     accepts the ifMatch and ifNoneMatch options, that map to the If-Match and
   *     If-None-Match headers respectively. See the remotestorage-01 specification
   *     for details on handling these headers. It returns a promise, fulfilled with
   *     the same values as the one for #get().
   *   - #delete() takes a path and the ifMatch option as well. It returns a promise
   *     fulfilled with the same values as the one for #get().
   *
   * In addition to this, the WireClient has some compatibility features to work with
   * remotestorage 2012.04 compatible storages. For example it will cache revisions
   * from folder listings in-memory and return them accordingly as the "revision"
   * parameter in response to #get() requests. Similarly it will return 404 when it
   * receives an empty folder listing, to mimic remotestorage-01 behavior. Note
   * that it is not always possible to know the revision beforehand, hence it may
   * be undefined at times (especially for caching-roots).
   */

  var hasLocalStorage;
  var SETTINGS_KEY = "remotestorage:wireclient";

  var API_2012 = 1, API_00 = 2, API_01 = 3, API_02 = 4, API_HEAD = 5;

  var STORAGE_APIS = {
    'draft-dejong-remotestorage-00': API_00,
    'draft-dejong-remotestorage-01': API_01,
    'draft-dejong-remotestorage-02': API_02,
    'https://www.w3.org/community/rww/wiki/read-write-web-00#simple': API_2012
  };

  var isArrayBufferView;

  if (typeof(ArrayBufferView) === 'function') {
    isArrayBufferView = function(object) { return object && (object instanceof ArrayBufferView); };
  } else {
    var arrayBufferViews = [
      Int8Array, Uint8Array, Int16Array, Uint16Array,
      Int32Array, Uint32Array, Float32Array, Float64Array
    ];
    isArrayBufferView = function(object) {
      for (var i=0;i<8;i++) {
        if (object instanceof arrayBufferViews[i]) {
          return true;
        }
      }
      return false;
    };
  }

  function readBinaryData(content, mimeType, callback) {
    var blob = new Blob([content], { type: mimeType });
    var reader = new FileReader();
    reader.addEventListener("loadend", function() {
      callback(reader.result); // reader.result contains the contents of blob as a typed array
    });
    reader.readAsArrayBuffer(blob);
  }

  function cleanPath(path) {
    return path.replace(/\/+/g, '/').split('/').map(encodeURIComponent).join('/');
  }

  function isFolder(path) {
    return (path.substr(-1) === '/');
  }

  function isFolderDescription(body) {
    return ((body['@context'] === 'http://remotestorage.io/spec/folder-description')
             && (typeof(body['items']) === 'object'));
  }

  var onErrorCb;

  /**
   * Class : RemoteStorage.WireClient
   **/
  RS.WireClient = function(rs) {
    this.connected = false;
    /**
     * Event: change
     *   never fired for some reason
     *
     * Event: connected
     *   fired when the wireclient connect method realizes that it is
     *   in posession of a token and a href
     **/
    RS.eventHandling(this, 'change', 'connected', 'wire-busy', 'wire-done');

    onErrorCb = function(error){
      if (error instanceof RemoteStorage.Unauthorized) {
        this.configure(undefined, undefined, undefined, null);
      }
    }.bind(this);
    rs.on('error', onErrorCb);
    if (hasLocalStorage) {
      var settings;
      try { settings = JSON.parse(localStorage[SETTINGS_KEY]); } catch(e) {}
      if (settings) {
        setTimeout(function() {
          this.configure(settings.userAddress, settings.href, settings.storageApi, settings.token);
        }.bind(this), 0);
      }
    }

    this._revisionCache = {};

    if (this.connected) {
      setTimeout(this._emit.bind(this), 0, 'connected');
    }
  };

  RS.WireClient.REQUEST_TIMEOUT = 30000;

  RS.WireClient.prototype = {
    /**
     * Property: token
     *
     * Holds the bearer token of this WireClient, as obtained in the OAuth dance
     *
     * Example:
     *   (start code)
     *
     *   remoteStorage.remote.token
     *   // -> 'DEADBEEF01=='
     */

    /**
     * Property: href
     *
     * Holds the server's base URL, as obtained in the Webfinger discovery
     *
     * Example:
     *   (start code)
     *
     *   remoteStorage.remote.href
     *   // -> 'https://storage.example.com/users/jblogg/'
     */

    /**
     * Property: storageApi
     *
     * Holds the spec version the server claims to be compatible with
     *
     * Example:
     *   (start code)
     *
     *   remoteStorage.remote.storageApi
     *   // -> 'draft-dejong-remotestorage-01'
     */

    _request: function(method, uri, token, headers, body, getEtag, fakeRevision) {
      if ((method === 'PUT' || method === 'DELETE') && uri[uri.length - 1] === '/') {
        throw "Don't " + method + " on directories!";
      }

      var promise = promising();
      var revision;
      var reqType;
      var self = this;

      headers['Authorization'] = 'Bearer ' + token;

      this._emit('wire-busy', {
        method: method,
        isFolder: isFolder(uri)
      });

      RS.WireClient.request(method, uri, {
        body: body,
        headers: headers
      }, function(error, response) {
        if (error) {
          self._emit('wire-done', {
            method: method,
            isFolder: isFolder(uri),
            success: false
          });
          promise.reject(error);
        } else {
          self._emit('wire-done', {
            method: method,
            isFolder: isFolder(uri),
            success: true
          });
          if ([401, 403, 404, 412].indexOf(response.status) >= 0) {
            promise.fulfill(response.status);
          } else if ([201, 204, 304].indexOf(response.status) >= 0 ||
                     (response.status === 200 && method !== 'GET')) {
            revision = response.getResponseHeader('ETag');
            promise.fulfill(response.status, undefined, undefined, revision);
          } else {
            var mimeType = response.getResponseHeader('Content-Type');
            var body;
            if (getEtag) {
              revision = response.getResponseHeader('ETag');
            } else {
              revision = response.status === 200 ? fakeRevision : undefined;
            }

            if ((! mimeType) || mimeType.match(/charset=binary/)) {
              RS.WireClient.readBinaryData(response.response, mimeType, function(result) {
                promise.fulfill(response.status, result, mimeType, revision);
              });
            } else {
              if (mimeType && mimeType.match(/^application\/json/)) {
                body = JSON.parse(response.responseText);
              } else {
                body = response.responseText;
              }
              promise.fulfill(response.status, body, mimeType, revision);
            }
          }
        }
      });
      return promise;
    },

    configure: function(userAddress, href, storageApi, token) {
      if (typeof(userAddress) !== 'undefined') {
        this.userAddress = userAddress;
      }
      if (typeof(href) !== 'undefined') {
        this.href = href;
      }
      if (typeof(storageApi) !== 'undefined') {
        this.storageApi = storageApi;
      }
      if (typeof(token) !== 'undefined') {
        this.token = token;
      }
      if (typeof(this.storageApi) !== 'undefined') {
        this._storageApi = STORAGE_APIS[this.storageApi] || API_HEAD;
        this.supportsRevs = this._storageApi >= API_00;
      }
      if (this.href && this.token) {
        this.connected = true;
        this._emit('connected');
      } else {
        this.connected = false;
      }
      if (hasLocalStorage) {
        localStorage[SETTINGS_KEY] = JSON.stringify({
          userAddress: this.userAddress,
          href: this.href,
          token: this.token,
          storageApi: this.storageApi
        });
      }
      RS.WireClient.configureHooks.forEach(function(hook) {
        hook.call(this);
      }.bind(this));
    },

    get: function(path, options) {
      if (!this.connected) {
        throw new Error("not connected (path: " + path + ")");
      }
      if (!options) { options = {}; }
      var headers = {};
      if (this.supportsRevs) {
        if (options.ifNoneMatch) {
          headers['If-None-Match'] = options.ifNoneMatch;
        }
      } else if (options.ifNoneMatch) {
        var oldRev = this._revisionCache[path];
      }
      var promise = this._request('GET', this.href + cleanPath(path), this.token, headers,
                            undefined, this.supportsRevs, this._revisionCache[path]);
      if (!isFolder(path)) {
        return promise;
      } else {
        return promise.then(function(status, body, contentType, revision) {
          var listing = {};

          // New folder listing received
          if (status === 200 && typeof(body) === 'object') {
            // Empty folder listing of any spec
            if (Object.keys(body).length === 0) {
              status = 404;
            }
            // >= 02 spec
            else if (isFolderDescription(body)) {
              for (var item in body.items) {
                this._revisionCache[path + item] = body.items[item].ETag;
              }
              listing = body.items;
            }
            // < 02 spec
            else {
              Object.keys(body).forEach(function(key){
                this._revisionCache[path + key] = body[key];
                listing[key] = {"ETag": body[key]};
              }.bind(this));
            }
            return promising().fulfill(status, listing, contentType, revision);
          }
          // No folder listing received
          else if (status === 404) {
            return promising().fulfill(404);
          }
          // Cached folder listing received
          else if (status === 304) {
            return promising().fulfill(status, body, contentType, revision);
          }
          // Faulty folder listing received
          else {
            var error = new Error("Received faulty folder response for: "+path);
            return promising().reject(error);
          }
        }.bind(this));
      }
    },

    put: function(path, body, contentType, options) {
      if (!this.connected) {
        throw new Error("not connected (path: " + path + ")");
      }
      if (!options) { options = {}; }
      if (!contentType.match(/charset=/)) {
        contentType += '; charset=' + ((body instanceof ArrayBuffer || isArrayBufferView(body)) ? 'binary' : 'utf-8');
      }
      var headers = { 'Content-Type': contentType };
      if (this.supportsRevs) {
        if (options.ifMatch) {
          headers['If-Match'] = options.ifMatch;
        }
        if (options.ifNoneMatch) {
          headers['If-None-Match'] = options.ifNoneMatch;
        }
      }
      return this._request('PUT', this.href + cleanPath(path), this.token,
                     headers, body, this.supportsRevs);
    },

    'delete': function(path, options) {
      if (!this.connected) {
        throw new Error("not connected (path: " + path + ")");
      }
      if (!options) { options = {}; }
      var headers = {};
      if (this.supportsRevs) {
        if (options.ifMatch) {
          headers['If-Match'] = options.ifMatch;
        }
      }
      return this._request('DELETE', this.href + cleanPath(path), this.token,
                     headers,
                     undefined, this.supportsRevs);
    }
  };

  // Shared cleanPath used by Dropbox
  RS.WireClient.cleanPath = cleanPath;

  // Shared isArrayBufferView used by WireClient and Dropbox
  RS.WireClient.isArrayBufferView = isArrayBufferView;

  RS.WireClient.readBinaryData = readBinaryData;

  // Shared request function used by WireClient, GoogleDrive and Dropbox.
  RS.WireClient.request = function(method, url, options, callback) {
    RemoteStorage.log(method, url);

    callback = callback.bind(this);

    var timedOut = false;

    var timer = setTimeout(function() {
      timedOut = true;
      callback('timeout');
    }, RS.WireClient.REQUEST_TIMEOUT);

    var xhr = new XMLHttpRequest();
    xhr.open(method, url, true);

    if (options.responseType) {
      xhr.responseType = options.responseType;
    }
    if (options.headers) {
      for (var key in options.headers) {
        xhr.setRequestHeader(key, options.headers[key]);
      }
    }

    xhr.onload = function() {
      if (timedOut) { return; }
      clearTimeout(timer);
      callback(null, xhr);
    };

    xhr.onerror = function(error) {
      if (timedOut) { return; }
      clearTimeout(timer);
      callback(error);
    };

    var body = options.body;

    if (typeof(body) === 'object') {
      if (isArrayBufferView(body)) {
        /* alright. */
        //FIXME empty block
      }
      else if (body instanceof ArrayBuffer) {
        body = new Uint8Array(body);
      } else {
        body = JSON.stringify(body);
      }
    }
    xhr.send(body);
  };

  Object.defineProperty(RemoteStorage.WireClient.prototype, 'storageType', {
    get: function() {
      if (this.storageApi) {
        var spec = this.storageApi.match(/draft-dejong-(remotestorage-\d\d)/);
        return spec ? spec[1] : '2012.04';
      }
    }
  });

  RS.WireClient.configureHooks = [];

  RS.WireClient._rs_init = function(remoteStorage) {
    hasLocalStorage = remoteStorage.localStorageAvailable();
    remoteStorage.remote = new RS.WireClient(remoteStorage);
  };

  RS.WireClient._rs_supported = function() {
    return !! global.XMLHttpRequest;
  };

  RS.WireClient._rs_cleanup = function(remoteStorage){
    if (hasLocalStorage){
      delete localStorage[SETTINGS_KEY];
    }
    remoteStorage.removeEventListener('error', onErrorCb);
  };

})(typeof(window) !== 'undefined' ? window : global);
<|endoftext|>


<commit_before>// ==========================================================================
// Project:   xTuple Postbooks - Business Management System Framework
// Copyright: ©2012 OpenMFG LLC, d/b/a xTuple
// ==========================================================================

/*globals XM */

/**
  @class

  Used to consolidate receivable and cash receipt detail into a single object.

  @extends SC.Object
*/
XM.CashReceiptApplication = SC.Object.extend(XT.Logging,
  /** @scope XM.CashReceiptApplication.prototype */ {

  cashReceipt: null,

  cashReceiptDetail: null,

  receivable: null,

  /**
    The amount to be applied in the money of the receivable.

    @type XM.Money
  */
  appliedMoney: null,

  /** @private */
  appliedMoneyExchangeRate: 1,

  /** @private */
  appliedMoneyExchangeRateBinding: SC.Binding.from('*appliedMoney.exchangeRate').oneWay().noDelay(),

  /** @private */
  isLoadingCashReceiptExchangeRate: false,

  /** @private */
  isLoadingCashReceiptExchangeRateBinding: SC.Binding.from('*cashReceipt.appliedMoney.isLoading').oneWay().noDelay(),

  /** @private */
  isLoadingReceivableExchangeRate: false,

  /** @private */
  isLoadingReceivableExchangeRateBinding: SC.Binding.from('*appliedMoney.isLoading').oneWay().noDelay(),

  /**
    The value of all pending applications in the receivable's currency.
  */
  pending: 0,

  /** @private */
  pendingBinding: SC.Binding.from('*receivable.pending').oneWay().noDelay(),

  // .................................................
  // CALCULATED PROPERTIES
  //

  /**
    The amount to be applied in the currency of the cash receipt.

    @type Number
  */
  applied: function() {
    return this.getPath('cashReceiptDetail.amount') || 0;
  }.property('cashReceiptDetail').cacheable(),

  /**
    The discount to be applied in the currency of the cash receipt.

    @type Number
  */
  discount: function() {
    return this.getPath('cashReceiptDetail.discount') || 0;
  }.property('cashReceiptDetail').cacheable(),

  /**
    The balance due on the receivable in the receivable's currency including pending
    applications.

    @type Number
  */
  balance: function() {
    var receivable = this.get('receivable'),
        balance = receivable.get('balance'),
        pending = receivable.get('pending');
    return SC.Math.round(balance - pending, XT.MONEY_SCALE);
  }.property('pending'),

  // .................................................
  // METHODS
  //

  init: function() {
    arguments.callee.base.apply(this, arguments);
    this.set('appliedMoney', XM.Money.create()); // bindings set up by observers
    this.set('appliedMoney', XM.Money.create());
  },

  /**
    Apply an amount from the cash receipt associated with this application
    to the receivable associtated with this application.

    @param {Number} amount
    @param {Number} discount
  */
  apply: function(amount, discount) {
    if (this.getPath('cashReceipt.isPosted')) return;
    this._xm_amountPending = amount;
    this._xm_discountPending = discount;
    this._xm_apply();
  },

  /**
    Apply as much as possible of the cash receipt to the balance of the receivable.

    @param {Function} optional - callback
  */
  applyBalance: function(callback) {
    this._xm_applyBalancePending = true;
    this._xm_pendingCallback = callback;
    this._xm_applyBalance();
  },

  /**
    Clear this application.
  */
  clear: function() {
    var cashReceipt = this.get('cashReceipt'),
        details = cashReceipt.get('details'),
        detail = this.get('cashReceiptDetail'),
        id = detail.get('id'),
        applied = this.get('applied') * -1,
        discount = this.get('discount') * -1,
        status = detail.get('status'), K = SC.Record;

    // remove related pending application record
    this._xm_removePending();

    // remove the detail from this object
    if (detail) {
      if (status == K.READY_NEW) details.removeObject(detail);
      detail.destroy();

      // update the cash receipt totals
      cashReceipt.updateApplied(applied);
      cashReceipt.updateDiscount(discount);
    }
    this.set('cashReceiptDetail', null);
  },

  /** @private
    Creates a pending application record on the receivable so we get a correct total for "all pending"
    applications.

    @param {Number} total amount applied
  */
  _xm_createPending: function() {
    var amount = this.getPath('appliedMoney.localValue'),
        detail = this.get('cashReceiptDetail'),
        receivable = this.get('receivable'),
        store = receivable.get('store'),
        applications = receivable.get('pendingApplications'),
        recordType = XM.ReceivablePendingApplication,
        storeKey, pending;

    // bail if nothing to do
    if (!detail) return;

    this.log('Creating pending application');

    // create a pending application record (info only, the datasource will ignore this)
    storeKey = store.loadRecord(recordType, {
      guid: detail.get('id'),
      pendingApplicationType: recordType.CASH_RECEIPT,
      receivable: receivable,
      amount: amount
    });
    pending = store.materializeRecord(storeKey);

    // bind the ids of detail (which may have a temporory id at this time)
    // we may need to reference this later if this application gets cleared
    SC.Binding.from('id', detail).to('id', pending).oneWay().noDelay().connect();

    // push new pending record into pending applications array
    applications.pushObject(pending);
  },

  /** @private
    Removes the pending application record associated with detail on this object from the receivable.
  */
  _xm_removePending: function() {
    var applications = this.getPath('receivable.pendingApplications'),
        id = this.getPath('cashReceiptDetail.id'), pending;
    pending = applications.findProperty('id', id);
    if (pending) {
      // just remove, don't destroy because we might reintroduce later
      applications.removeObject(pending);
    }
  },

  // .................................................
  // OBSERVERS
  //

  appliedDidChange: function() {
    if (this.get('isLoadingReceivableExchangeRate') ||
        this.get('isLoadingCashReceiptExchangeRate')) return;
    var crCurrencyRate = this.getPath('cashReceipt.appliedMoney.exchangeRate'),
        arCurrencyRate = this.getPath('appliedMoney.exchangeRate'),
        applied = this.get('applied'),
        discount = this.get('discount'),
        arApplied = SC.Math.round((applied + discount) * arCurrencyRate / crCurrencyRate, XT.MONEY_SCALE);

    // update applied money
    this.setPathIfChanged('appliedMoney.localValue', arApplied);

    // rebuild pending application records
    var amount = this.get('applied') + this.get('discount');
    this._xm_removePending();
    this._xm_createPending();
  }.observes('applied', 'appliedMoneyExchangeRate', 'isLoadingReceivableExchangeRate', 'isLoadingCashReceiptExchangeRate'),

  cashReceiptDidChange: function() {
    var cashReceipt = this.get('cashReceipt'),
        appliedMoney = this.get('appliedMoney');
    SC.Binding.from('applicationDate', cashReceipt)
          .to('effective', appliedMoney)
          .oneWay().noDelay().connect();
  }.observes('cashReceipt'),

  receivableDidChange: function() {
    var currency = this.getPath('receivable.currency'),
        appliedMoney = this.get('appliedMoney');
    appliedMoney.set('currency', currency);
  }.observes('receivable'),

  /** @private
    Execute any pending request to apply cash. If the client is waiting
    for exchange rate data, punt and try again when loading is complete.
  */
  _xm_apply: function() {
    // bail if nothing to do
    if (SC.none(this._xm_amountPending)) return;

    // can't process if exchange rates are still loading
    if (this.get('isLoadingCashReceiptExchangeRate') ||
        this.get('isLoadingReceivableExchangeRate')) {
      this.log('Exiting apply pending until exchange rates loaded.');
      return;
    }

    // setup
    var cashReceipt = this.get('cashReceipt'),
        crCurrencyRate = cashReceipt.get('appliedMoney.exchangeRate'),
        arCurrencyRate = this.get('appliedMoney.exchangeRate'),
        detail = this.get('cashReceiptDetail'),
        receivable = this.get('receivable'),
        documentType = receivable.get('documentType'),
        applied = this.get('applied'),
        balance = this.get('balance'),
        store = receivable.get('store'),
        discount = this._xm_discountPending || 0,
        amount = this._xm_amountPending;

    // calculate balance in cash receipt currency
    balance = SC.Math.round(balance * crCurrencyRate / arCurrencyRate + applied, XT.MONEY_SCALE);

    // values must be valid
    if (amount < 0 || discount < 0 ||
        amount + discount > balance) {
      this.error('Can not apply a value greater than the balance');
      return false;
    }

    // credits need sense reversed
    if (documentType === XM.Receivable.CREDIT_MEMO ||
        documentType === XM.Receivable.CUSTOMER_DEPOSIT) {
      amount = amount * -1;
      discount = 0; // should never be a discount on credit
    }

    // clear the old detail
    if (detail) this.clear();

    // create a new detail
    detail = store.createRecord(XM.CashReceiptDetail, {});
    detail.set('receivable', receivable)
          .set('amount', amount)
          .set('discount', discount);
    cashReceipt.get('details').pushObject(detail);

    // associate detail to this application
    this.set('cashReceiptDetail', detail);

    // notify caller if applicable
    if (this._xm_callbackPending) this._xm_callbackPending.call(detail);

    // reset
    this._xm_amountPending = undefined;
    this._xm_discountPending = undefined;
    this._xm_callbackPending = undefined;
  }.observes('isLoadingReceivableExchangeRate', 'isLoadingCashReceiptExchangeRate'),

  /** @private
    Execute any pending request to apply cash balance. If the client is waiting
    for exchange rate data, punt and try again when loading is complete.
  */
  _xm_applyBalance: function() {
    // bail if nothing to do
    if (!this._xm_applyBalancePending) return;

    // can't process if exchange rates are still loading
    if (this.get('isLoadingCashReceiptExchangeRate') ||
        this.get('isLoadingReceivableExchangeRate')) {
      this.log('Exiting apply pending until exchange rates loaded.');
      return;
    }

    //setup
    var applied = this.get('applied'),
        cashReceipt = this.get('cashReceipt'),
        crBalance = cashReceipt.get('balance'),
        crCurrencyRate = cashReceipt.get('appliedMoney.exchangeRate'),
        arCurrencyRate = this.get('appliedMoney.exchangeRate'),
        receivable = this.get('receivable'),
        documentType = receivable.get('documentType'),
        arBalance = this.get('balance'),
        documentDate = receivable.get('documentDate'),
        terms = receivable.get('terms'),
        discountDate = terms ? terms.calculateDiscountDate(documentDate) : null,
        discountPercent = terms ? terms.get('discountPercent') / 100 : 0,
        discount = 0, amount;

    // determine balance we could apply in cash receipt currency
    amount = SC.Math.round(crBalance + applied, XT.MONEY_SCALE);
    balance = SC.Math.round(arBalance * crCurrencyRate / arCurrencyRate + applied, XT.MONEY_SCALE);

    // bail out if nothing to do
    if (balance === 0 || amount === 0) return;

    // calculate discount if applicable
    if (balance > 0 && discountDate &&
        SC.DateTime.compareDate(documentDate, discountDate) <= 0) {
      discount = SC.Math.round(balance * discountPercent, XT.MONEY_SCALE);
    }

    // adjust the amount or discount as appropriate and apply
    if (documentType === XM.Receivable.INVOICE ||
        documentType === XM.Receivable.DEBIT_MEMO) {
      if (arBalance <= amount + discount) {
        amount = SC.Math.round(arBalance - discount, XT.MONEY_SCALE);
      } else {
        discount = SC.Math.round((amount / (1 - discountPercent)) - amount, XT.MONEY_SCALE);
      }
    } else {
      amount = arBalance;
      discount = 0;
    }
    if (amount) return this.apply(amount, discount);

    this._xm_applyBalancePending = false;
  }.observes('isLoadingReceivableExchangeRate', 'isLoadingCashReceiptExchangeRate'),

});

<commit_msg>more fixes.<commit_after>// ==========================================================================
// Project:   xTuple Postbooks - Business Management System Framework
// Copyright: ©2012 OpenMFG LLC, d/b/a xTuple
// ==========================================================================

/*globals XM */

/**
  @class

  Used to consolidate receivable and cash receipt detail into a single object.

  @extends SC.Object
*/
XM.CashReceiptApplication = SC.Object.extend(XT.Logging,
  /** @scope XM.CashReceiptApplication.prototype */ {

  cashReceipt: null,

  cashReceiptDetail: null,

  receivable: null,

  /**
    The amount to be applied in the money of the receivable.

    @type XM.Money
  */
  appliedMoney: null,

  /** @private */
  appliedMoneyExchangeRate: 1,

  /** @private */
  appliedMoneyExchangeRateBinding: SC.Binding.from('*appliedMoney.exchangeRate').oneWay().noDelay(),

  /** @private */
  isLoadingCashReceiptExchangeRate: false,

  /** @private */
  isLoadingCashReceiptExchangeRateBinding: SC.Binding.from('*cashReceipt.amountMoney.isLoading').oneWay().noDelay(),

  /** @private */
  isLoadingReceivableExchangeRate: false,

  /** @private */
  isLoadingReceivableExchangeRateBinding: SC.Binding.from('*appliedMoney.isLoading').oneWay().noDelay(),

  /**
    The value of all pending applications in the receivable's currency.
  */
  pending: 0,

  /** @private */
  pendingBinding: SC.Binding.from('*receivable.pending').oneWay().noDelay(),

  // .................................................
  // CALCULATED PROPERTIES
  //

  /**
    The amount to be applied in the currency of the cash receipt.

    @type Number
  */
  applied: function() {
    return this.getPath('cashReceiptDetail.amount') || 0;
  }.property('cashReceiptDetail').cacheable(),

  /**
    The discount to be applied in the currency of the cash receipt.

    @type Number
  */
  discount: function() {
    return this.getPath('cashReceiptDetail.discount') || 0;
  }.property('cashReceiptDetail').cacheable(),

  /**
    The balance due on the receivable in the receivable's currency including pending
    applications.

    @type Number
  */
  balance: function() {
    var receivable = this.get('receivable'),
        balance = receivable.get('balance'),
        pending = receivable.get('pending');
    return SC.Math.round(balance - pending, XT.MONEY_SCALE);
  }.property('pending'),

  // .................................................
  // METHODS
  //

  init: function() {
    arguments.callee.base.apply(this, arguments);
    this.set('appliedMoney', XM.Money.create()); // bindings set up by observers
  },

  /**
    Apply an amount from the cash receipt associated with this application
    to the receivable associtated with this application.

    @param {Number} amount
    @param {Number} discount
  */
  apply: function(amount, discount) {
    if (this.getPath('cashReceipt.isPosted')) return;
    this._xm_amountPending = amount;
    this._xm_discountPending = discount;
    this._xm_apply();
  },

  /**
    Apply as much as possible of the cash receipt to the balance of the receivable.

    @param {Function} optional - callback
  */
  applyBalance: function(callback) {
    this._xm_applyBalancePending = true;
    this._xm_pendingCallback = callback;
    this._xm_applyBalance();
  },

  /**
    Clear this application.
  */
  clear: function() {
    var cashReceipt = this.get('cashReceipt'),
        details = cashReceipt.get('details'),
        detail = this.get('cashReceiptDetail'),
        id = detail.get('id'),
        applied = this.get('applied') * -1,
        discount = this.get('discount') * -1,
        status = detail.get('status'), K = SC.Record;

    // remove related pending application record
    this._xm_removePending();

    // remove the detail from this object
    if (detail) {
      if (status == K.READY_NEW) details.removeObject(detail);
      detail.destroy();

      // update the cash receipt totals
      cashReceipt.updateApplied(applied);
      cashReceipt.updateDiscount(discount);
    }
    this.set('cashReceiptDetail', null);
  },

  /** @private
    Creates a pending application record on the receivable so we get a correct total for "all pending"
    applications.

    @param {Number} total amount applied
  */
  _xm_createPending: function() {
    var amount = this.getPath('appliedMoney.localValue'),
        detail = this.get('cashReceiptDetail'),
        receivable = this.get('receivable'),
        store = receivable.get('store'),
        applications = receivable.get('pendingApplications'),
        recordType = XM.ReceivablePendingApplication,
        storeKey, pending;

    // bail if nothing to do
    if (!detail) return;

    this.log('Creating pending application');

    // create a pending application record (info only, the datasource will ignore this)
    storeKey = store.loadRecord(recordType, {
      guid: detail.get('id'),
      pendingApplicationType: recordType.CASH_RECEIPT,
      receivable: receivable,
      amount: amount
    });
    pending = store.materializeRecord(storeKey);

    // bind the ids of detail (which may have a temporory id at this time)
    // we may need to reference this later if this application gets cleared
    SC.Binding.from('id', detail).to('id', pending).oneWay().noDelay().connect();

    // push new pending record into pending applications array
    applications.pushObject(pending);
  },

  /** @private
    Removes the pending application record associated with detail on this object from the receivable.
  */
  _xm_removePending: function() {
    var applications = this.getPath('receivable.pendingApplications'),
        id = this.getPath('cashReceiptDetail.id'), pending;
    if (id) pending = applications.findProperty('id', id);
    if (pending) {
      // just remove, don't destroy because we might reintroduce later
      applications.removeObject(pending);
    }
  },

  // .................................................
  // OBSERVERS
  //

  appliedDidChange: function() {
    if (this.get('isLoadingReceivableExchangeRate') ||
        this.get('isLoadingCashReceiptExchangeRate')) return;
    var crCurrencyRate = this.getPath('cashReceipt.amountMoney.exchangeRate'),
        arCurrencyRate = this.getPath('appliedMoney.exchangeRate'),
        applied = this.get('applied'),
        discount = this.get('discount'),
        arApplied = SC.Math.round((applied + discount) * arCurrencyRate / crCurrencyRate, XT.MONEY_SCALE);

    // update applied money
    this.setPathIfChanged('appliedMoney.localValue', arApplied);

    // rebuild pending application records
    var amount = this.get('applied') + this.get('discount');
    this._xm_removePending();
    this._xm_createPending();
  }.observes('applied', 'appliedMoneyExchangeRate', 'isLoadingReceivableExchangeRate', 'isLoadingCashReceiptExchangeRate'),

  cashReceiptDidChange: function() {
    var cashReceipt = this.get('cashReceipt'),
        appliedMoney = this.get('appliedMoney');
    SC.Binding.from('applicationDate', cashReceipt)
          .to('effective', appliedMoney)
          .oneWay().noDelay().connect();
  }.observes('cashReceipt'),

  receivableDidChange: function() {
    var currency = this.getPath('receivable.currency'),
        appliedMoney = this.get('appliedMoney');
    appliedMoney.set('currency', currency);
  }.observes('receivable'),

  /** @private
    Execute any pending request to apply cash. If the client is waiting
    for exchange rate data, punt and try again when loading is complete.
  */
  _xm_apply: function() {
    // bail if nothing to do
    if (SC.none(this._xm_amountPending)) return;

    // can't process if exchange rates are still loading
    if (this.get('isLoadingCashReceiptExchangeRate') ||
        this.get('isLoadingReceivableExchangeRate')) {
      this.log('Exiting apply pending until exchange rates loaded.');
      return;
    }

    // setup
    var cashReceipt = this.get('cashReceipt'),
        crCurrencyRate = cashReceipt.get('appliedMoney.exchangeRate'),
        arCurrencyRate = this.get('appliedMoney.exchangeRate'),
        detail = this.get('cashReceiptDetail'),
        receivable = this.get('receivable'),
        documentType = receivable.get('documentType'),
        applied = this.get('applied'),
        balance = this.get('balance'),
        store = receivable.get('store'),
        discount = this._xm_discountPending || 0,
        amount = this._xm_amountPending;

    // calculate balance in cash receipt currency
    balance = SC.Math.round(balance * crCurrencyRate / arCurrencyRate + applied, XT.MONEY_SCALE);

    // values must be valid
    if (amount < 0 || discount < 0 ||
        amount + discount > balance) {
      this.error('Can not apply a value greater than the balance');
      return false;
    }

    // credits need sense reversed
    if (documentType === XM.Receivable.CREDIT_MEMO ||
        documentType === XM.Receivable.CUSTOMER_DEPOSIT) {
      amount = amount * -1;
      discount = 0; // should never be a discount on credit
    }

    // clear the old detail
    if (detail) this.clear();

    // create a new detail
    detail = store.createRecord(XM.CashReceiptDetail, {});
    detail.set('receivable', receivable)
          .set('amount', amount)
          .set('discount', discount);
    cashReceipt.get('details').pushObject(detail);

    // associate detail to this application
    this.set('cashReceiptDetail', detail);

    // notify caller if applicable
    if (this._xm_callbackPending) this._xm_callbackPending.call(detail);

    // reset
    this._xm_amountPending = undefined;
    this._xm_discountPending = undefined;
    this._xm_callbackPending = undefined;
  }.observes('isLoadingReceivableExchangeRate', 'isLoadingCashReceiptExchangeRate'),

  /** @private
    Execute any pending request to apply cash balance. If the client is waiting
    for exchange rate data, punt and try again when loading is complete.
  */
  _xm_applyBalance: function() {
    // bail if nothing to do
    if (!this._xm_applyBalancePending) return;

    // can't process if exchange rates are still loading
    if (this.get('isLoadingCashReceiptExchangeRate') ||
        this.get('isLoadingReceivableExchangeRate')) {
      this.log('Exiting apply pending until exchange rates loaded.');
      return;
    }

    //setup
    var applied = this.get('applied'),
        cashReceipt = this.get('cashReceipt'),
        crBalance = cashReceipt.get('balance'),
        crCurrencyRate = cashReceipt.get('appliedMoney.exchangeRate'),
        arCurrencyRate = this.get('appliedMoney.exchangeRate'),
        receivable = this.get('receivable'),
        documentType = receivable.get('documentType'),
        arBalance = this.get('balance'),
        documentDate = receivable.get('documentDate'),
        terms = receivable.get('terms'),
        discountDate = terms ? terms.calculateDiscountDate(documentDate) : null,
        discountPercent = terms ? terms.get('discountPercent') / 100 : 0,
        discount = 0, amount;

    // determine balance we could apply in cash receipt currency
    amount = SC.Math.round(crBalance + applied, XT.MONEY_SCALE);
    balance = SC.Math.round(arBalance * crCurrencyRate / arCurrencyRate + applied, XT.MONEY_SCALE);

    // bail out if nothing to do
    if (balance === 0 || amount === 0) return;

    // calculate discount if applicable
    if (balance > 0 && discountDate &&
        SC.DateTime.compareDate(documentDate, discountDate) <= 0) {
      discount = SC.Math.round(balance * discountPercent, XT.MONEY_SCALE);
    }

    // adjust the amount or discount as appropriate and apply
    if (documentType === XM.Receivable.INVOICE ||
        documentType === XM.Receivable.DEBIT_MEMO) {
      if (arBalance <= amount + discount) {
        amount = SC.Math.round(arBalance - discount, XT.MONEY_SCALE);
      } else {
        discount = SC.Math.round((amount / (1 - discountPercent)) - amount, XT.MONEY_SCALE);
      }
    } else {
      amount = arBalance;
      discount = 0;
    }
    if (amount) return this.apply(amount, discount);

    this._xm_applyBalancePending = false;
  }.observes('isLoadingReceivableExchangeRate', 'isLoadingCashReceiptExchangeRate'),

});

<|endoftext|>