<commit_before>var fs     = require("fs");
var http   = require("http");
var os     = require("os");
var Stream = require("stream");
var URL    = require("url");
var util   = require("util");

// The starting point is everything that works in any environment (browser OR
// Node.js)
var Lazy = require("./lazy.js");

/**
 * @constructor
 */
function StreamedSequence(stream) {
  this.stream = stream;
}

StreamedSequence.prototype = new Lazy.StreamLikeSequence();

StreamedSequence.prototype.openStream = function(callback) {
  this.stream.resume();
  callback(this.stream);
};

/**
 * Handles every chunk of data in this sequence.
 *
 * @param {function(string):*} fn The function to call on each chunk of data as
 *     it's read from the stream. Return false from the function to stop reading
 *     the stream.
 */
StreamedSequence.prototype.each = function(fn) {
  var encoding = this.encoding || "utf-8";

  var handle = new Lazy.AsyncHandle();

  this.openStream(function(stream) {
    var listener = function(e) {
      try {
        if (fn(e) === false) {
          stream.removeListener("data", listener);
        }
      } catch (e) {
        handle.errorCallback(e);
      }
    };

    if (stream.setEncoding) {
      stream.setEncoding(encoding);
    }

    stream.on("data", listener);

    stream.on("end", function() {
      handle.completeCallback();
    });
  });

  return handle;
};

/**
 * Creates a {@link Sequence} of lines as they are read from a file.
 *
 * @return {Sequence} A sequence comprising the lines in the underlying file, as
 *     they are read.
 */
StreamedSequence.prototype.lines = function() {
  return this.split(os.EOL || "\n");
};

function FileStreamSequence(path, encoding) {
  this.path = path;
  this.encoding = encoding;
}

FileStreamSequence.prototype = new StreamedSequence();

FileStreamSequence.prototype.openStream = function(callback) {
  var stream = fs.createReadStream(this.path, { autoClose: true });
  callback(stream);
};

/**
 * Creates a {@link Sequence} from a file stream, whose elements are chunks of
 * data as the stream is read. This sequence works asynchronously, so
 * synchronous methods such as {@code indexOf}, {@code any}, and {@code toArray}
 * won't work.
 *
 * @param {string} path A path to a file.
 * @param {string} encoding The text encoding of the file (e.g., "utf-8").
 * @return {Sequence} The streamed sequence.
 */
Lazy.readFile = function(path, encoding) {
  return new FileStreamSequence(path, encoding);
};

function HttpStreamSequence(url, encoding) {
  this.url = url;
  this.encoding = encoding;
}

HttpStreamSequence.prototype = new StreamedSequence();

HttpStreamSequence.prototype.openStream = function(callback) {
  http.get(URL.parse(this.url), callback);
};

/**
 * Creates a {@link Sequence} from an HTTP stream, whose elements are chunks of
 * data as the stream is read. This sequence works asynchronously, so
 * synchronous methods such as {@code indexOf}, {@code any}, and {@code toArray}
 * won't work.
 *
 * @param {string} url The URL for the HTTP request.
 * @return {Sequence} The streamed sequence.
 */
Lazy.makeHttpRequest = function(url) {
  return new HttpStreamSequence(url);
};

if (typeof Stream.Readable !== "undefined") {
  Lazy.Sequence.prototype.toStream = function toStream(options) {
    return new LazyStream(this, options);
  };

  function LazyStream(sequence, options) {
    options = Lazy(options || {})
      .extend({ objectMode: true })
      .toObject();

    Stream.Readable.call(this, options);

    this.sequence = sequence;
    this.started  = false;
  }

  util.inherits(LazyStream, Stream.Readable);

  LazyStream.prototype._read = function() {
    var self = this;

    if (!this.started) {
      var handle = this.sequence.each(function(e, i) {
        return self.push(e, i);
      });
      if (handle instanceof Lazy.AsyncHandle) {
        handle.onComplete(function() {
          self.push(null);
        });
      }
      this.started = true;
    }
  };
}

/*
 * Add support for `Lazy(Stream)`.
 */
if (!Lazy.extensions) {
  Lazy.extensions = [];
}

Lazy.extensions.push(function(source) {
  if (source instanceof Stream) {
    return new StreamedSequence(source);
  }
});

module.exports = Lazy;
<commit_msg>added simple #pipe convenience method<commit_after>var fs     = require("fs");
var http   = require("http");
var os     = require("os");
var Stream = require("stream");
var URL    = require("url");
var util   = require("util");

// The starting point is everything that works in any environment (browser OR
// Node.js)
var Lazy = require("./lazy.js");

/**
 * @constructor
 */
function StreamedSequence(stream) {
  this.stream = stream;
}

StreamedSequence.prototype = new Lazy.StreamLikeSequence();

StreamedSequence.prototype.openStream = function(callback) {
  this.stream.resume();
  callback(this.stream);
};

/**
 * Handles every chunk of data in this sequence.
 *
 * @param {function(string):*} fn The function to call on each chunk of data as
 *     it's read from the stream. Return false from the function to stop reading
 *     the stream.
 */
StreamedSequence.prototype.each = function(fn) {
  var encoding = this.encoding || "utf-8";

  var handle = new Lazy.AsyncHandle();

  this.openStream(function(stream) {
    var listener = function(e) {
      try {
        if (fn(e) === false) {
          stream.removeListener("data", listener);
        }
      } catch (e) {
        handle.errorCallback(e);
      }
    };

    if (stream.setEncoding) {
      stream.setEncoding(encoding);
    }

    stream.on("data", listener);

    stream.on("end", function() {
      handle.completeCallback();
    });
  });

  return handle;
};

/**
 * Creates a {@link Sequence} of lines as they are read from a file.
 *
 * @return {Sequence} A sequence comprising the lines in the underlying file, as
 *     they are read.
 */
StreamedSequence.prototype.lines = function() {
  return this.split(os.EOL || "\n");
};

function FileStreamSequence(path, encoding) {
  this.path = path;
  this.encoding = encoding;
}

FileStreamSequence.prototype = new StreamedSequence();

FileStreamSequence.prototype.openStream = function(callback) {
  var stream = fs.createReadStream(this.path, { autoClose: true });
  callback(stream);
};

/**
 * Creates a {@link Sequence} from a file stream, whose elements are chunks of
 * data as the stream is read. This sequence works asynchronously, so
 * synchronous methods such as {@code indexOf}, {@code any}, and {@code toArray}
 * won't work.
 *
 * @param {string} path A path to a file.
 * @param {string} encoding The text encoding of the file (e.g., "utf-8").
 * @return {Sequence} The streamed sequence.
 */
Lazy.readFile = function(path, encoding) {
  return new FileStreamSequence(path, encoding);
};

function HttpStreamSequence(url, encoding) {
  this.url = url;
  this.encoding = encoding;
}

HttpStreamSequence.prototype = new StreamedSequence();

HttpStreamSequence.prototype.openStream = function(callback) {
  http.get(URL.parse(this.url), callback);
};

/**
 * Creates a {@link Sequence} from an HTTP stream, whose elements are chunks of
 * data as the stream is read. This sequence works asynchronously, so
 * synchronous methods such as {@code indexOf}, {@code any}, and {@code toArray}
 * won't work.
 *
 * @param {string} url The URL for the HTTP request.
 * @return {Sequence} The streamed sequence.
 */
Lazy.makeHttpRequest = function(url) {
  return new HttpStreamSequence(url);
};

if (typeof Stream.Readable !== "undefined") {
  Lazy.Sequence.prototype.toStream = function toStream(options) {
    return new LazyStream(this, options);
  };

  Lazy.Sequence.prototype.pipe = function pipe(destination) {
    this.toStream().pipe(destination);
  };

  function LazyStream(sequence, options) {
    options = Lazy(options || {})
      .extend({ objectMode: true })
      .toObject();

    Stream.Readable.call(this, options);

    this.sequence = sequence;
    this.started  = false;
  }

  util.inherits(LazyStream, Stream.Readable);

  LazyStream.prototype._read = function() {
    var self = this;

    if (!this.started) {
      var handle = this.sequence.each(function(e, i) {
        return self.push(e, i);
      });
      if (handle instanceof Lazy.AsyncHandle) {
        handle.onComplete(function() {
          self.push(null);
        });
      }
      this.started = true;
    }
  };
}

/*
 * Add support for `Lazy(Stream)`.
 */
if (!Lazy.extensions) {
  Lazy.extensions = [];
}

Lazy.extensions.push(function(source) {
  if (source instanceof Stream) {
    return new StreamedSequence(source);
  }
});

module.exports = Lazy;
<|endoftext|>


<commit_before>/**
 * Created by BK on 21/06/16.
 *
 * @flow
 */

 'use strict';

 import React, { Component } from 'react';
 import {
     Text,
     View,
     Image,
     ListView,
     TouchableHighlight,
     StyleSheet
 } from 'react-native';

type Props = {
    row: {
        thumbURI: string,
        title: string,
        desc: string,
        timestamp: string
    }
}

export default class NotificationsRow extends Component {
    props: Props;

    render() {
        return (
            <View>
                <TouchableHighlight>
                    <View style={styles.container}>
                        <View style={styles.thumbContainer}>
                            <Image
                                style={styles.thumb}
                                source={{uri: this.props.row.thumbURI}}
                            />
                        </View>
                        <View style={styles.textContainer}>
                            <Text style={styles.title}>{this.props.row.title}</Text>
                            <Text style={styles.desc}>{this.props.row.desc}</Text>
                            <Text style={styles.reward}>{this.props.row.timestamp}</Text>
                        </View>
                    </View>
                </TouchableHighlight>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        flexDirection: 'row',
        backgroundColor: '#dedede'
    },
    thumbContainer: {
        flex: 1,
        backgroundColor: '#ededed',
        justifyContent: 'center',
        alignItems: 'center'
    },
    textContainer: {
        flex: 4,
        flexDirection: 'column',
        justifyContent: 'center',
        alignItems: 'center'
    },
    title: {
        fontSize: 20,
    },
    desc: {

    },
    timestamp: {

    },
    thumb: {
        height: 45,
        width: 45,
        borderRadius: 22, // why dafuq is 22 circular??
    },
})
<commit_msg>Use global style props for thumbnails<commit_after>/**
 * Created by BK on 21/06/16.
 *
 * @flow
 */

 'use strict';

 import React, { Component } from 'react';
 import {
     Text,
     View,
     Image,
     ListView,
     TouchableHighlight,
     StyleSheet
 } from 'react-native';
 import GlobalStyles from '../../common/GlobalStyles'

type Props = {
    row: {
        thumbURI: string,
        title: string,
        desc: string,
        timestamp: string
    }
}

export default class NotificationsRow extends Component {
    props: Props;

    render() {
        return (
            <View>
                <TouchableHighlight>
                    <View style={styles.container}>
                        <View style={styles.thumbContainer}>
                            <Image
                                style={[GlobalStyles.thumbnail, styles.thumbnail]}
                                source={{uri: this.props.row.thumbURI}}
                            />
                        </View>
                        <View style={styles.textContainer}>
                            <Text style={styles.title}>{this.props.row.title}</Text>
                            <Text style={styles.desc}>{this.props.row.desc}</Text>
                            <Text style={styles.reward}>{this.props.row.timestamp}</Text>
                        </View>
                    </View>
                </TouchableHighlight>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        flexDirection: 'row',
        backgroundColor: '#dedede'
    },
    thumbContainer: {
        flex: 1,
        backgroundColor: '#ededed',
        justifyContent: 'center',
        alignItems: 'center'
    },
    textContainer: {
        flex: 4,
        flexDirection: 'column',
        justifyContent: 'center',
        alignItems: 'center'
    },
    title: {
        fontSize: 20,
    },
    desc: {

    },
    timestamp: {

    },
    thumbnail: {

    },
})
<|endoftext|>


<commit_before>{% load i18n %}

/* Part API functions
 * Requires api.js to be loaded first
 */

function toggleStar(options) {
    /* Toggle the 'starred' status of a part.
     * Performs AJAX queries and updates the display on the button.
     *
     * options:
     * - button: ID of the button (default = '#part-star-icon')
     * - part: pk of the part object
     * - user: pk of the user
     */

    var url = '/api/part/star/';

    inventreeGet(
        url,
        {
            part: options.part,
            user: options.user,
        },
        {
            success: function(response) {
                if (response.length == 0) {
                    // Zero length response = star does not exist
                    // So let's add one!
                    inventreePut(
                        url,
                        {
                            part: options.part,
                            user: options.user,
                        },
                        {
                            method: 'POST',
                            success: function(response, status) {
                                $(options.button).addClass('icon-yellow');
                            },
                        }
                    );
                } else {
                    var pk = response[0].pk;
                    // There IS a star (delete it!)
                    inventreePut(
                        url + pk + "/",
                        {
                        },
                        {
                            method: 'DELETE',
                            success: function(response, status) {
                                $(options.button).removeClass('icon-yellow');
                            },
                        }
                    );
                }
            },
        }
    );
}

function makePartIcons(part, options={}) {
    /* Render a set of icons for the given part.
     */

    var html = '';

    if (part.trackable) {
        html += makeIconBadge('fa-directions', '{% trans "Trackable part" %}');
    }

    if (part.virtual) {
        html += makeIconBadge('fa-ghost', '{% trans "Virtual part" %}');
    }

    if (part.is_template) {
        html += makeIconBadge('fa-clone', '{% trans "Template part" %}');
    }

    if (part.assembly) {
        html += makeIconBadge('fa-tools', '{% trans "Assembled part" %}');
    }

    if (part.starred) {
        html += makeIconBadge('fa-star', '{% trans "Starred part" %}');
    }

    if (part.salable) {
        html += makeIconBadge('fa-dollar-sign', title='{% trans "Salable part" %}');
    }

    if (!part.active) {
        html += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`;
    }

    return html;

}

function loadPartVariantTable(table, partId, options={}) {
    /* Load part variant table
     */

    var params = options.params || {};

    params.ancestor = partId;

    // Load filters
    var filters = loadTableFilters("variants");

    for (var key in params) {
        filters[key] = params[key];
    }

    setupFilterList("variants", $(table));

    var cols = [
        {
            field: 'pk',
            title: 'ID',
            visible: false,
            switchable: false,
        },
        {
            field: 'name',
            title: '{% trans "Name" %}',
            switchable: false,
            formatter: function(value, row, index, field) {
                var html = '';

                var name = '';

                if (row.IPN) {
                    name += row.IPN;
                    name += ' | ';
                }

                name += value;

                if (row.revision) {
                    name += ' | ';
                    name += row.revision;
                }

                if (row.is_template) {
                    name = '' + name + '';
                }

                html += imageHoverIcon(row.thumbnail);
                html += renderLink(name, `/part/${row.pk}/`);

                if (row.trackable) {
                    html += makeIconBadge('fa-directions', '{% trans "Trackable part" %}');
                }

                if (row.virtual) {
                    html += makeIconBadge('fa-ghost', '{% trans "Virtual part" %}');
                }

                if (row.is_template) {
                    html += makeIconBadge('fa-clone', '{% trans "Template part" %}');
                }

                if (row.assembly) {
                    html += makeIconBadge('fa-tools', '{% trans "Assembled part" %}');
                }

                if (!row.active) {
                    html += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`;
                }

                return html;
            },
        },
        {
            field: 'IPN',
            title: '{% trans "IPN" %}',
        },
        {
            field: 'revision',
            title: '{% trans "Revision" %}',
        },
        {
            field: 'description',
            title: '{% trans "Description" %}',
        },
        {
            field: 'in_stock',
            title: '{% trans "Stock" %}',
            formatter: function(value, row) {
                return renderLink(value, `/part/${row.pk}/stock/`);
            }
        }
    ];

    table.inventreeTable({
        url: "{% url 'api-part-list' %}",
        name: 'partvariants',
        showColumns: true,
        original: params,
        queryParams: filters,
        formatNoMatches: function() { return "{% trans "No variants found" %}"; },
        columns: cols,
        treeEnable: true,
        rootParentId: partId,
        parentIdField: 'variant_of',
        idField: 'pk',
        uniqueId: 'pk',
        treeShowField: 'name',
        sortable: true,
        search: true,
        onPostBody: function() {
            table.treegrid({
                treeColumn: 0,
            });

            table.treegrid('collapseAll');
        }
    });
}

function loadSimplePartTable(table, url, options={}) {

    options.disableFilters = true;

    loadPartTable(table, url, options);
}

function loadParametricPartTable(table, options={}) {
    /* Load parametric table for part parameters
     *
     * Args:
     *  - table: HTML reference to the table
     *  - table_headers: Unique parameters found in category
     *  - table_data: Parameters data
     */

    var table_headers = options.headers
    var table_data = options.data

    var columns = [];

    for (header of table_headers) {
        if (header === 'part') {
            columns.push({
                field: header,
                title: '{% trans 'Part' %}',
                sortable: true,
                sortName: 'name',
                formatter: function(value, row, index, field) {

                    var name = '';

                    if (row.IPN) {
                        name += row.IPN + ' | ' + row.name;
                    } else {
                        name += row.name;
                    }

                    return renderLink(name, '/part/' + row.pk + '/');
                }
            });
        } else if (header === 'description') {
            columns.push({
                field: header,
                title: '{% trans 'Description' %}',
                sortable: true,
            });
        } else {
            columns.push({
                field: header,
                title: header,
                sortable: true,
                filterControl: 'input',
                /* TODO: Search icons are not displayed */
                /*clear: 'fa-times icon-red',*/
            });
        }
    }

    $(table).inventreeTable({
        sortName: 'part',
        queryParams: table_headers,
        groupBy: false,
        name: options.name || 'parametric',
        formatNoMatches: function() { return "{% trans "No parts found" %}"; },
        columns: columns,
        showColumns: true,
        data: table_data,
        filterControl: true,
    });
}

function loadPartTable(table, url, options={}) {
    /* Load part listing data into specified table.
     *
     * Args:
     *  - table: HTML reference to the table
     *  - url: Base URL for API query
     *  - options: object containing following (optional) fields
     *      checkbox: Show the checkbox column
     *      query: extra query params for API request
     *      buttons: If provided, link buttons to selection status of this table
     *      disableFilters: If true, disable custom filters
     */

    // Ensure category detail is included
    options.params['category_detail'] = true;

    var params = options.params || {};

    var filters = {};

    if (!options.disableFilters) {
        filters = loadTableFilters("parts");
    }

    for (var key in params) {
        filters[key] = params[key];
    }

    setupFilterList("parts", $(table));

    var columns = [
        {
            field: 'pk',
            title: 'ID',
            visible: false,
            switchable: false,
            searchable: false,
        }
    ];

    if (options.checkbox) {
        columns.push({
            checkbox: true,
            title: '{% trans "Select" %}',
            searchable: false,
            switchable: false,
        });
    }

    columns.push({
        field: 'IPN',
        title: 'IPN',
        sortable: true,
    }),

    columns.push({
        field: 'name',
        title: '{% trans "Part" %}',
        sortable: true,
        switchable: false,
        formatter: function(value, row, index, field) {

            var name = '';

            if (row.IPN) {
                name += row.IPN;
                name += ' | ';
            }

            name += value;

            if (row.revision) {
                name += ' | ';
                name += row.revision;
            }

            if (row.is_template) {
                name = '' + name + '';
            }

            var display = imageHoverIcon(row.thumbnail) + renderLink(name, '/part/' + row.pk + '/');

            display += makePartIcons(row);

            return display;
        }
    });

    columns.push({
        sortable: true,
        field: 'description',
        title: '{% trans "Description" %}',
        formatter: function(value, row, index, field) {

            if (row.is_template) {
                value = '' + value + '';
            }

            return value;
        }
    });

    columns.push({
        sortable: true,
        field: 'category_detail',
        title: '{% trans "Category" %}',
        formatter: function(value, row, index, field) {
            if (row.category) {
                return renderLink(value.pathstring, "/part/category/" + row.category + "/");
            }
            else {
                return '{% trans "No category" %}';
            }
        }
    });

    columns.push({
        field: 'in_stock',
        title: '{% trans "Stock" %}',
        searchable: false,
        sortable: true,
        formatter: function(value, row, index, field) {
            var link = "stock";

            if (value) {
                // There IS stock available for this part

                // Is stock "low" (below the 'minimum_stock' quantity)?
                if (row.minimum_stock && row.minimum_stock > value) {
                    value += "{% trans "Low stock" %}";
                }

            } else if (row.on_order) {
                // There is no stock available, but stock is on order
                value = "0{% trans "On Order" %}: " + row.on_order + "";
                link = "orders";
            } else if (row.building) {
                // There is no stock available, but stock is being built
                value = "0{% trans "Building" %}: " + row.building + "";
                link = "builds";
            } else {
                // There is no stock available
                value = "0{% trans "No Stock" %}";
            }

            return renderLink(value, '/part/' + row.pk + "/" + link + "/");
        }
    });

    $(table).inventreeTable({
        url: url,
        sortName: 'name',
        method: 'get',
        queryParams: filters,
        groupBy: false,
        name: options.name || 'part',
        original: params,
        formatNoMatches: function() { return "{% trans "No parts found" %}"; },
        columns: columns,
        showColumns: true,
    });

    if (options.buttons) {
        linkButtonsToSelection($(table), options.buttons);
    }

    /* Button callbacks for part table buttons */

    $("#multi-part-order").click(function() {
        var selections = $(table).bootstrapTable("getSelections");

        var parts = [];

        selections.forEach(function(item) {
            parts.push(item.pk);
        });

        launchModalForm("/order/purchase-order/order-parts/", {
            data: {
                parts: parts,
            },
        });
    });

    $("#multi-part-category").click(function() {
        var selections = $(table).bootstrapTable("getSelections");

        var parts = [];

        selections.forEach(function(item) {
            parts.push(item.pk);
        });

        launchModalForm("/part/set-category/", {
            data: {
                parts: parts,
            },
            reload: true,
        });
    });

    $('#multi-part-export').click(function() {
        var selections = $(table).bootstrapTable("getSelections");

        var parts = '';

        selections.forEach(function(item) {
            parts += item.pk;
            parts += ',';
        });

        location.href = '/part/export/?parts=' + parts;
    });
}

function yesNoLabel(value) {
    if (value) {
        return `<span class='label label-green'>{% trans "YES" %}</span>`;
    } else {
        return `<span class='label label-yellow'>{% trans "NO" %}</span>`;
    }
}

function loadPartTestTemplateTable(table, options) {
    /*
     * Load PartTestTemplate table.
     */

    var params = options.params || {};

    var part = options.part || null;

    var filterListElement = options.filterList || '#filter-list-parttests';

    var filters = loadTableFilters("parttests");

    var original = {};

    for (var key in params) {
        original[key] = params[key];
    }

    setupFilterList("parttests", table, filterListElement);

    // Override the default values, or add new ones
    for (var key in params) {
        filters[key] = params[key];
    }

    table.inventreeTable({
        method: 'get',
        formatNoMatches: function() {
            return '{% trans "No test templates matching query" %}';
        },
        url: "{% url 'api-part-test-template-list' %}",
        queryParams: filters,
        name: 'testtemplate',
        original: original,
        columns: [
            {
                field: 'pk',
                title: 'ID',
                visible: false,
            },
            {
                field: 'test_name',
                title: "{% trans "Test Name" %}",
                sortable: true,
            },
            {
                field: 'description',
                title: "{% trans "Description" %}",
            },
            {
                field: 'required',
                title: "{% trans 'Required' %}",
                sortable: true,
                formatter: function(value) {
                    return yesNoLabel(value);
                }
            },
            {
                field: 'requires_value',
                title: "{% trans "Requires Value" %}",
                formatter: function(value) {
                    return yesNoLabel(value);
                }
            },
            {
                field: 'requires_attachment',
                title: "{% trans "Requires Attachment" %}",
                formatter: function(value) {
                    return yesNoLabel(value);
                }
            },
            {
                field: 'buttons',
                formatter: function(value, row) {
                    var pk = row.pk;

                    if (row.part == part) {
                        var html = `<div class='btn-group float-right' role='group'>`;

                        html += makeIconButton('fa-edit icon-blue', 'button-test-edit', pk, '{% trans "Edit test result" %}');
                        html += makeIconButton('fa-trash-alt icon-red', 'button-test-delete', pk, '{% trans "Delete test result" %}');

                        html += `</div>`;

                        return html;
                    } else {
                        return '{% trans "This test is defined for a parent part" %}';
                    }
                }
            }
        ]
    });
}
<commit_msg>Render link rather than just text<commit_after>{% load i18n %}

/* Part API functions
 * Requires api.js to be loaded first
 */

function toggleStar(options) {
    /* Toggle the 'starred' status of a part.
     * Performs AJAX queries and updates the display on the button.
     *
     * options:
     * - button: ID of the button (default = '#part-star-icon')
     * - part: pk of the part object
     * - user: pk of the user
     */

    var url = '/api/part/star/';

    inventreeGet(
        url,
        {
            part: options.part,
            user: options.user,
        },
        {
            success: function(response) {
                if (response.length == 0) {
                    // Zero length response = star does not exist
                    // So let's add one!
                    inventreePut(
                        url,
                        {
                            part: options.part,
                            user: options.user,
                        },
                        {
                            method: 'POST',
                            success: function(response, status) {
                                $(options.button).addClass('icon-yellow');
                            },
                        }
                    );
                } else {
                    var pk = response[0].pk;
                    // There IS a star (delete it!)
                    inventreePut(
                        url + pk + "/",
                        {
                        },
                        {
                            method: 'DELETE',
                            success: function(response, status) {
                                $(options.button).removeClass('icon-yellow');
                            },
                        }
                    );
                }
            },
        }
    );
}

function makePartIcons(part, options={}) {
    /* Render a set of icons for the given part.
     */

    var html = '';

    if (part.trackable) {
        html += makeIconBadge('fa-directions', '{% trans "Trackable part" %}');
    }

    if (part.virtual) {
        html += makeIconBadge('fa-ghost', '{% trans "Virtual part" %}');
    }

    if (part.is_template) {
        html += makeIconBadge('fa-clone', '{% trans "Template part" %}');
    }

    if (part.assembly) {
        html += makeIconBadge('fa-tools', '{% trans "Assembled part" %}');
    }

    if (part.starred) {
        html += makeIconBadge('fa-star', '{% trans "Starred part" %}');
    }

    if (part.salable) {
        html += makeIconBadge('fa-dollar-sign', title='{% trans "Salable part" %}');
    }

    if (!part.active) {
        html += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`;
    }

    return html;

}

function loadPartVariantTable(table, partId, options={}) {
    /* Load part variant table
     */

    var params = options.params || {};

    params.ancestor = partId;

    // Load filters
    var filters = loadTableFilters("variants");

    for (var key in params) {
        filters[key] = params[key];
    }

    setupFilterList("variants", $(table));

    var cols = [
        {
            field: 'pk',
            title: 'ID',
            visible: false,
            switchable: false,
        },
        {
            field: 'name',
            title: '{% trans "Name" %}',
            switchable: false,
            formatter: function(value, row, index, field) {
                var html = '';

                var name = '';

                if (row.IPN) {
                    name += row.IPN;
                    name += ' | ';
                }

                name += value;

                if (row.revision) {
                    name += ' | ';
                    name += row.revision;
                }

                if (row.is_template) {
                    name = '' + name + '';
                }

                html += imageHoverIcon(row.thumbnail);
                html += renderLink(name, `/part/${row.pk}/`);

                if (row.trackable) {
                    html += makeIconBadge('fa-directions', '{% trans "Trackable part" %}');
                }

                if (row.virtual) {
                    html += makeIconBadge('fa-ghost', '{% trans "Virtual part" %}');
                }

                if (row.is_template) {
                    html += makeIconBadge('fa-clone', '{% trans "Template part" %}');
                }

                if (row.assembly) {
                    html += makeIconBadge('fa-tools', '{% trans "Assembled part" %}');
                }

                if (!row.active) {
                    html += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`;
                }

                return html;
            },
        },
        {
            field: 'IPN',
            title: '{% trans "IPN" %}',
        },
        {
            field: 'revision',
            title: '{% trans "Revision" %}',
        },
        {
            field: 'description',
            title: '{% trans "Description" %}',
        },
        {
            field: 'in_stock',
            title: '{% trans "Stock" %}',
            formatter: function(value, row) {
                return renderLink(value, `/part/${row.pk}/stock/`);
            }
        }
    ];

    table.inventreeTable({
        url: "{% url 'api-part-list' %}",
        name: 'partvariants',
        showColumns: true,
        original: params,
        queryParams: filters,
        formatNoMatches: function() { return "{% trans "No variants found" %}"; },
        columns: cols,
        treeEnable: true,
        rootParentId: partId,
        parentIdField: 'variant_of',
        idField: 'pk',
        uniqueId: 'pk',
        treeShowField: 'name',
        sortable: true,
        search: true,
        onPostBody: function() {
            table.treegrid({
                treeColumn: 0,
            });

            table.treegrid('collapseAll');
        }
    });
}

function loadSimplePartTable(table, url, options={}) {

    options.disableFilters = true;

    loadPartTable(table, url, options);
}

function loadParametricPartTable(table, options={}) {
    /* Load parametric table for part parameters
     *
     * Args:
     *  - table: HTML reference to the table
     *  - table_headers: Unique parameters found in category
     *  - table_data: Parameters data
     */

    var table_headers = options.headers
    var table_data = options.data

    var columns = [];

    for (header of table_headers) {
        if (header === 'part') {
            columns.push({
                field: header,
                title: '{% trans 'Part' %}',
                sortable: true,
                sortName: 'name',
                formatter: function(value, row, index, field) {

                    var name = '';

                    if (row.IPN) {
                        name += row.IPN + ' | ' + row.name;
                    } else {
                        name += row.name;
                    }

                    return renderLink(name, '/part/' + row.pk + '/');
                }
            });
        } else if (header === 'description') {
            columns.push({
                field: header,
                title: '{% trans 'Description' %}',
                sortable: true,
            });
        } else {
            columns.push({
                field: header,
                title: header,
                sortable: true,
                filterControl: 'input',
                /* TODO: Search icons are not displayed */
                /*clear: 'fa-times icon-red',*/
            });
        }
    }

    $(table).inventreeTable({
        sortName: 'part',
        queryParams: table_headers,
        groupBy: false,
        name: options.name || 'parametric',
        formatNoMatches: function() { return "{% trans "No parts found" %}"; },
        columns: columns,
        showColumns: true,
        data: table_data,
        filterControl: true,
    });
}

function loadPartTable(table, url, options={}) {
    /* Load part listing data into specified table.
     *
     * Args:
     *  - table: HTML reference to the table
     *  - url: Base URL for API query
     *  - options: object containing following (optional) fields
     *      checkbox: Show the checkbox column
     *      query: extra query params for API request
     *      buttons: If provided, link buttons to selection status of this table
     *      disableFilters: If true, disable custom filters
     */

    // Ensure category detail is included
    options.params['category_detail'] = true;

    var params = options.params || {};

    var filters = {};

    if (!options.disableFilters) {
        filters = loadTableFilters("parts");
    }

    for (var key in params) {
        filters[key] = params[key];
    }

    setupFilterList("parts", $(table));

    var columns = [
        {
            field: 'pk',
            title: 'ID',
            visible: false,
            switchable: false,
            searchable: false,
        }
    ];

    if (options.checkbox) {
        columns.push({
            checkbox: true,
            title: '{% trans "Select" %}',
            searchable: false,
            switchable: false,
        });
    }

    columns.push({
        field: 'IPN',
        title: 'IPN',
        sortable: true,
    }),

    columns.push({
        field: 'name',
        title: '{% trans "Part" %}',
        sortable: true,
        switchable: false,
        formatter: function(value, row, index, field) {

            var name = '';

            if (row.IPN) {
                name += row.IPN;
                name += ' | ';
            }

            name += value;

            if (row.revision) {
                name += ' | ';
                name += row.revision;
            }

            if (row.is_template) {
                name = '' + name + '';
            }

            var display = imageHoverIcon(row.thumbnail) + renderLink(name, '/part/' + row.pk + '/');

            display += makePartIcons(row);

            return display;
        }
    });

    columns.push({
        sortable: true,
        field: 'description',
        title: '{% trans "Description" %}',
        formatter: function(value, row, index, field) {

            if (row.is_template) {
                value = '' + value + '';
            }

            return value;
        }
    });

    columns.push({
        sortable: true,
        field: 'category_detail',
        title: '{% trans "Category" %}',
        formatter: function(value, row, index, field) {
            if (row.category) {
                return renderLink(value.pathstring, "/part/category/" + row.category + "/");
            }
            else {
                return '{% trans "No category" %}';
            }
        }
    });

    columns.push({
        field: 'in_stock',
        title: '{% trans "Stock" %}',
        searchable: false,
        sortable: true,
        formatter: function(value, row, index, field) {
            var link = "stock";

            if (value) {
                // There IS stock available for this part

                // Is stock "low" (below the 'minimum_stock' quantity)?
                if (row.minimum_stock && row.minimum_stock > value) {
                    value += "{% trans "Low stock" %}";
                }

            } else if (row.on_order) {
                // There is no stock available, but stock is on order
                value = "0{% trans "On Order" %}: " + row.on_order + "";
                link = "orders";
            } else if (row.building) {
                // There is no stock available, but stock is being built
                value = "0{% trans "Building" %}: " + row.building + "";
                link = "builds";
            } else {
                // There is no stock available
                value = "0{% trans "No Stock" %}";
            }

            return renderLink(value, '/part/' + row.pk + "/" + link + "/");
        }
    });

    $(table).inventreeTable({
        url: url,
        sortName: 'name',
        method: 'get',
        queryParams: filters,
        groupBy: false,
        name: options.name || 'part',
        original: params,
        formatNoMatches: function() { return "{% trans "No parts found" %}"; },
        columns: columns,
        showColumns: true,
    });

    if (options.buttons) {
        linkButtonsToSelection($(table), options.buttons);
    }

    /* Button callbacks for part table buttons */

    $("#multi-part-order").click(function() {
        var selections = $(table).bootstrapTable("getSelections");

        var parts = [];

        selections.forEach(function(item) {
            parts.push(item.pk);
        });

        launchModalForm("/order/purchase-order/order-parts/", {
            data: {
                parts: parts,
            },
        });
    });

    $("#multi-part-category").click(function() {
        var selections = $(table).bootstrapTable("getSelections");

        var parts = [];

        selections.forEach(function(item) {
            parts.push(item.pk);
        });

        launchModalForm("/part/set-category/", {
            data: {
                parts: parts,
            },
            reload: true,
        });
    });

    $('#multi-part-export').click(function() {
        var selections = $(table).bootstrapTable("getSelections");

        var parts = '';

        selections.forEach(function(item) {
            parts += item.pk;
            parts += ',';
        });

        location.href = '/part/export/?parts=' + parts;
    });
}

function yesNoLabel(value) {
    if (value) {
        return `<span class='label label-green'>{% trans "YES" %}</span>`;
    } else {
        return `<span class='label label-yellow'>{% trans "NO" %}</span>`;
    }
}

function loadPartTestTemplateTable(table, options) {
    /*
     * Load PartTestTemplate table.
     */

    var params = options.params || {};

    var part = options.part || null;

    var filterListElement = options.filterList || '#filter-list-parttests';

    var filters = loadTableFilters("parttests");

    var original = {};

    for (var key in params) {
        original[key] = params[key];
    }

    setupFilterList("parttests", table, filterListElement);

    // Override the default values, or add new ones
    for (var key in params) {
        filters[key] = params[key];
    }

    table.inventreeTable({
        method: 'get',
        formatNoMatches: function() {
            return '{% trans "No test templates matching query" %}';
        },
        url: "{% url 'api-part-test-template-list' %}",
        queryParams: filters,
        name: 'testtemplate',
        original: original,
        columns: [
            {
                field: 'pk',
                title: 'ID',
                visible: false,
            },
            {
                field: 'test_name',
                title: "{% trans "Test Name" %}",
                sortable: true,
            },
            {
                field: 'description',
                title: "{% trans "Description" %}",
            },
            {
                field: 'required',
                title: "{% trans 'Required' %}",
                sortable: true,
                formatter: function(value) {
                    return yesNoLabel(value);
                }
            },
            {
                field: 'requires_value',
                title: "{% trans "Requires Value" %}",
                formatter: function(value) {
                    return yesNoLabel(value);
                }
            },
            {
                field: 'requires_attachment',
                title: "{% trans "Requires Attachment" %}",
                formatter: function(value) {
                    return yesNoLabel(value);
                }
            },
            {
                field: 'buttons',
                formatter: function(value, row) {
                    var pk = row.pk;

                    if (row.part == part) {
                        var html = `<div class='btn-group float-right' role='group'>`;

                        html += makeIconButton('fa-edit icon-blue', 'button-test-edit', pk, '{% trans "Edit test result" %}');
                        html += makeIconButton('fa-trash-alt icon-red', 'button-test-delete', pk, '{% trans "Delete test result" %}');

                        html += `</div>`;

                        return html;
                    } else {
                        var text = '{% trans "This test is defined for a parent part" %}';

                        return renderLink(text, `/part/${row.part}/tests/`);
                    }
                }
            }
        ]
    });
}
<|endoftext|>


<commit_before>'use strict';

const util = require('util');
const EventEmitter = require('events').EventEmitter;
const request = require('request');
const Router = require('express').Router;
const FB_URL = 'https://graph.facebook.com/v2.6/';
const FB_MESSENGER_URL = `${FB_URL}/me/messages`;

const BUTTON_TYPE = {
    POSTBACK: 'postback',
    WEB_URL: 'web_url',
    ACCOUNT_LINK: 'account_link'
};

const TEMPLATE_TYPE = {
    BUTTON: 'button',
    RECEIPT: 'receipt',
    GENERIC: 'generic'
};

const ATTACHMENT_TYPE = {
    IMAGE: 'image',
    FILE: 'file',
    VIDEO: 'video',
    AUDIO: 'audio',
    TEMPLATE: 'template'
};

const NOTIFICATION_TYPE = {
    REGULAR: 'REGULAR',
    SILENT_PUSH: 'SILENT_PUSH',
    NO_PUSH: 'NO_PUSH'
};

const ACTION_TYPES = {
    MARK_SEEN: 'mark_seen',
    TYPING_ON: 'typing_on',
    TYPING_OFF: 'typing_off',
};

function Botly(options = {
    webHookPath: '/',
    notificationType: NOTIFICATION_TYPE.REGULAR
}) {
    if (!(this instanceof Botly)) {
        return new Botly(options);
    }

    if (!options || !options.accessToken) {
        throw new Error('Must provide accessToken');
    }
    this.accessToken = options.accessToken;
    this.verifyToken = options.verifyToken;
    this.webHookPath = options.webHookPath;
    this.notificationType = options.notificationType;
    EventEmitter.call(this);
}

util.inherits(Botly, EventEmitter);

Botly.prototype.router = function () {

    const router = Router();

    router.get(this.webHookPath, (req, res) => {
        if (req.query['hub.verify_token'] === this.verifyToken) {
            res.send(req.query['hub.challenge']);
        } else {
            res.send('Error, wrong validation token');
        }
    });

    router.post(this.webHookPath, (req, res) => {
        res.sendStatus(200); //return 200 to facebook no matter what
        try {
            this.handleMessage(req);
        } catch (ex) {
            this.emit('error', ex);
        }
    });

    return router;
};

Botly.prototype.getPSID = function (options, callback) {
    let accountLinkingToken = options;
    if (typeof options === 'object') {
        accountLinkingToken = options.token;
    }
    const USER_URL = `${FB_URL}me`;

    request.get(
        {
            url: USER_URL,
            qs: {
                fields: 'recipient',
                account_linking_token: accountLinkingToken,
                access_token: this.accessToken || options.accessToken
            },
            json: true

        }, (err, res, body) => {
            if (callback) {
                callback(err, body);
            }
        });
};

Botly.prototype.getUserProfile = function (options, callback) {
    let userId = options;
    if (typeof options === 'object') {
        userId = options.id;
    }
    const USER_URL = `${FB_URL}${userId}`;

    request.get(
        {
            url: USER_URL,
            qs: {
                fields: 'first_name,last_name,profile_pic,locale,timezone,gender',
                access_token: this.accessToken || options.accessToken
            },
            json: true

        }, (err, res, body) => {
            if (callback) {
                callback(err, body);
            }
        });
};

Botly.prototype.setGetStarted = function (options, callback) {
    const PAGE_URL = `${FB_URL}${options.pageId}/thread_settings`;

    request.post(
        {
            url: PAGE_URL,
            json: true,
            qs: {
                access_token: this.accessToken || options.accessToken
            },
            body: {
                setting_type: 'call_to_actions',
                thread_state: 'new_thread',
                call_to_actions: [
                    {
                        payload: options.payload
                    }
                ]
            }

        }, (err, res, body) => {
            if (callback) {
                callback(err, body);
            }
        });
};

Botly.prototype.setPersistentMenu = function (options, callback) {
    const PAGE_URL = `${FB_URL}${options.pageId}/thread_settings`;

    request.post(
        {
            url: PAGE_URL,
            json: true,
            qs: {
                access_token: this.accessToken || options.accessToken
            },
            body: {
                setting_type: 'call_to_actions',
                thread_state: 'existing_thread',
                call_to_actions: options.buttons
            }

        }, (err, res, body) => {
            if (callback) {
                callback(err, body);
            }
        });
};

Botly.prototype.send = function (options, callback) {
    options.notificationType = options.notificationType || this.notificationType;
    request.post(
        {
            url: FB_MESSENGER_URL,
            json: true,
            qs: {
                access_token: this.accessToken || options.accessToken
            },
            body: {
                recipient: {id: options.id},
                message: options.message,
                notification_type: options.notificationType
            }

        }, (err, res, body) => {
            if (callback) {
                callback(err, body);
            }
        });
    this.emit('sent', options.id, options.message);
};

Botly.prototype.sendAction = function (options, callback) {
    request.post(
        {
            url: FB_MESSENGER_URL,
            json: true,
            qs: {
                access_token: this.accessToken || options.accessToken
            },
            body: {
                recipient: {id: options.id},
                sender_action: options.action
            }

        }, (err, res, body) => {
            if (callback) {
                callback(err, body);
            }
        });
};

Botly.prototype.sendAttachment = function (options, callback) {
    options.message = options.message || {
            attachment: {
                type: options.type,
                payload: options.payload
            }
        };
    if (options.quick_replies) {
        options.message.quick_replies = options.quick_replies;
    }
    this.send(options, callback);
};

Botly.prototype.sendImage = function (options, callback) {
    options.payload = options.payload || {
            url: options.url
        };
    options.type = ATTACHMENT_TYPE.IMAGE;
    this.sendAttachment(options, callback);
};

Botly.prototype.sendText = function (options, callback) {
    options.message = options.message || {
            text: options.text
        };
    if (options.quick_replies) {
        options.message.quick_replies = options.quick_replies;
    }
    this.send(options, callback);
};

Botly.prototype.sendButtons = function (options, callback) {
    options.payload = this.createButtonTemplate(options.text, options.buttons);
    options.type = ATTACHMENT_TYPE.TEMPLATE;
    this.sendAttachment(options, callback);
};

Botly.prototype.sendGeneric = function (options, callback) {
    options.payload = this.createGenericTemplate(options.elements);
    options.type = ATTACHMENT_TYPE.TEMPLATE;
    this.sendAttachment(options, callback);
};

Botly.prototype.sendReceipt = function (options, callback) {
    options.payload.template_type = TEMPLATE_TYPE.RECEIPT;
    options.type = ATTACHMENT_TYPE.TEMPLATE;
    this.sendAttachment(options, callback);
};

Botly.prototype.createWebURLButton = function (title, url) {
    return {
        type: BUTTON_TYPE.WEB_URL,
        title: title,
        url: url
    };
};

Botly.prototype.createAccountLinkButton = function (url) {
    return {
        type: BUTTON_TYPE.ACCOUNT_LINK,
        url: url
    };
};

Botly.prototype.createPostbackButton = function (title, payload) {
    return {
        type: BUTTON_TYPE.POSTBACK,
        title: title,
        payload: payload
    };
};

Botly.prototype.createQuickReply = function (title, payload) {
    return {
        'content_type': 'text',
        'title': title,
        'payload': payload
    };
};

Botly.prototype.createButtonTemplate = function (text, buttons) {
    if (!Array.isArray(buttons)) {
        buttons = [buttons];
    }
    return {
        template_type: TEMPLATE_TYPE.BUTTON,
        text: text,
        buttons: buttons
    };
};

Botly.prototype.createGenericTemplate = function (elements) {
    if (!Array.isArray(elements)) {
        elements = [elements];
    }
    return {
        template_type: TEMPLATE_TYPE.GENERIC,
        elements: elements
    };
};

Botly.prototype.handleMessage = function (req) {
    const body = _clone(req.body);

    this.emit('complete_message', body);
    body.entry.forEach(entry => {
        entry.messaging.forEach(message => {
            _handleOptIn.call(this, message);
            _handleDelivery.call(this, message);
            _handlePostback.call(this, message);
            _handleAccountLinking.call(this, message);
            _handleUserMessage.call(this, message);
            _handleEcho.call(this, message);
        }, this);
    }, this);
};

function _handleOptIn(message) {
    if (message.optin) {
        let optin = message.optin.ref;
        this.emit('optin', message.sender.id, message, optin);
    }
}

function _handleDelivery(message) {
    if (message.delivery) {
        let mids = message.delivery.mids;
        this.emit('delivery', message.sender.id, message, mids);
    }
}

function _handlePostback(message) {
    if (message.postback || (message.message && !message.message.is_echo && message.message.quick_reply)) {
        let postback = (message.postback && message.postback.payload) || message.message.quick_reply.payload;
        this.emit('postback', message.sender.id, message, postback);
    }
}

function _handleAccountLinking(message) {
    if (message.account_linking) {
        let link = message.account_linking;
        this.emit('account_link', message.sender.id, message, link);
    }
}

function _handleUserMessage(message) {
    var value;
    if (message.message && !message.message.is_echo && message.message.text && !message.message.quick_reply) {
        value = {text: message.message.text};
    }

    if (message.message && !message.message.is_echo && message.message.attachments) {
        let attachments = {};
        message.message.attachments.forEach((attachment) => {
            if (attachment.type && attachment.payload) {
                attachments[attachment.type] = attachments[attachment.type] || [];
                attachments[attachment.type].push(attachment.payload.url || attachment.payload.coordinates);
            }
        });
        value = {attachments: attachments};
    }

    if (value) {
        this.emit('message', message.sender.id, message, value);
    }
}

function _handleEcho(message) {
    if (message.message && message.message.is_echo) {
        this.emit('echo', message.sender.id, message, message.message);
    }
}

function _clone(obj) {
    return JSON.parse(JSON.stringify(obj));
}

Botly.CONST = {
    BUTTON_TYPE: BUTTON_TYPE,
    TEMPLATE_TYPE: TEMPLATE_TYPE,
    ATTACHMENT_TYPE: ATTACHMENT_TYPE,
    NOTIFICATION_TYPE: NOTIFICATION_TYPE,
    ACTION_TYPES: ACTION_TYPES
};

module.exports = Botly;
<commit_msg>Revert: small default options modification (e991c1433662a3391598bcbd82306c01e63aeae3)<commit_after>'use strict';

const util = require('util');
const EventEmitter = require('events').EventEmitter;
const request = require('request');
const Router = require('express').Router;
const FB_URL = 'https://graph.facebook.com/v2.6/';
const FB_MESSENGER_URL = `${FB_URL}/me/messages`;

const BUTTON_TYPE = {
    POSTBACK: 'postback',
    WEB_URL: 'web_url',
    ACCOUNT_LINK: 'account_link'
};

const TEMPLATE_TYPE = {
    BUTTON: 'button',
    RECEIPT: 'receipt',
    GENERIC: 'generic'
};

const ATTACHMENT_TYPE = {
    IMAGE: 'image',
    FILE: 'file',
    VIDEO: 'video',
    AUDIO: 'audio',
    TEMPLATE: 'template'
};

const NOTIFICATION_TYPE = {
    REGULAR: 'REGULAR',
    SILENT_PUSH: 'SILENT_PUSH',
    NO_PUSH: 'NO_PUSH'
};

const ACTION_TYPES = {
    MARK_SEEN: 'mark_seen',
    TYPING_ON: 'typing_on',
    TYPING_OFF: 'typing_off',
};

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

    if (!options || !options.accessToken) {
        throw new Error('Must provide accessToken');
    }
    this.accessToken = options.accessToken;
    this.verifyToken = options.verifyToken;
    this.webHookPath = options.webHookPath || '/';
    this.notificationType = options.notificationType || NOTIFICATION_TYPE.REGULAR;
    EventEmitter.call(this);
}

util.inherits(Botly, EventEmitter);

Botly.prototype.router = function () {

    const router = Router();

    router.get(this.webHookPath, (req, res) => {
        if (req.query['hub.verify_token'] === this.verifyToken) {
            res.send(req.query['hub.challenge']);
        } else {
            res.send('Error, wrong validation token');
        }
    });

    router.post(this.webHookPath, (req, res) => {
        res.sendStatus(200); //return 200 to facebook no matter what
        try {
            this.handleMessage(req);
        } catch (ex) {
            this.emit('error', ex);
        }
    });

    return router;
};

Botly.prototype.getPSID = function (options, callback) {
    let accountLinkingToken = options;
    if (typeof options === 'object') {
        accountLinkingToken = options.token;
    }
    const USER_URL = `${FB_URL}me`;

    request.get(
        {
            url: USER_URL,
            qs: {
                fields: 'recipient',
                account_linking_token: accountLinkingToken,
                access_token: this.accessToken || options.accessToken
            },
            json: true

        }, (err, res, body) => {
            if (callback) {
                callback(err, body);
            }
        });
};

Botly.prototype.getUserProfile = function (options, callback) {
    let userId = options;
    if (typeof options === 'object') {
        userId = options.id;
    }
    const USER_URL = `${FB_URL}${userId}`;

    request.get(
        {
            url: USER_URL,
            qs: {
                fields: 'first_name,last_name,profile_pic,locale,timezone,gender',
                access_token: this.accessToken || options.accessToken
            },
            json: true

        }, (err, res, body) => {
            if (callback) {
                callback(err, body);
            }
        });
};

Botly.prototype.setGetStarted = function (options, callback) {
    const PAGE_URL = `${FB_URL}${options.pageId}/thread_settings`;

    request.post(
        {
            url: PAGE_URL,
            json: true,
            qs: {
                access_token: this.accessToken || options.accessToken
            },
            body: {
                setting_type: 'call_to_actions',
                thread_state: 'new_thread',
                call_to_actions: [
                    {
                        payload: options.payload
                    }
                ]
            }

        }, (err, res, body) => {
            if (callback) {
                callback(err, body);
            }
        });
};

Botly.prototype.setPersistentMenu = function (options, callback) {
    const PAGE_URL = `${FB_URL}${options.pageId}/thread_settings`;

    request.post(
        {
            url: PAGE_URL,
            json: true,
            qs: {
                access_token: this.accessToken || options.accessToken
            },
            body: {
                setting_type: 'call_to_actions',
                thread_state: 'existing_thread',
                call_to_actions: options.buttons
            }

        }, (err, res, body) => {
            if (callback) {
                callback(err, body);
            }
        });
};

Botly.prototype.send = function (options, callback) {
    options.notificationType = options.notificationType || this.notificationType;
    request.post(
        {
            url: FB_MESSENGER_URL,
            json: true,
            qs: {
                access_token: this.accessToken || options.accessToken
            },
            body: {
                recipient: {id: options.id},
                message: options.message,
                notification_type: options.notificationType
            }

        }, (err, res, body) => {
            if (callback) {
                callback(err, body);
            }
        });
    this.emit('sent', options.id, options.message);
};

Botly.prototype.sendAction = function (options, callback) {
    request.post(
        {
            url: FB_MESSENGER_URL,
            json: true,
            qs: {
                access_token: this.accessToken || options.accessToken
            },
            body: {
                recipient: {id: options.id},
                sender_action: options.action
            }

        }, (err, res, body) => {
            if (callback) {
                callback(err, body);
            }
        });
};

Botly.prototype.sendAttachment = function (options, callback) {
    options.message = options.message || {
            attachment: {
                type: options.type,
                payload: options.payload
            }
        };
    if (options.quick_replies) {
        options.message.quick_replies = options.quick_replies;
    }
    this.send(options, callback);
};

Botly.prototype.sendImage = function (options, callback) {
    options.payload = options.payload || {
            url: options.url
        };
    options.type = ATTACHMENT_TYPE.IMAGE;
    this.sendAttachment(options, callback);
};

Botly.prototype.sendText = function (options, callback) {
    options.message = options.message || {
            text: options.text
        };
    if (options.quick_replies) {
        options.message.quick_replies = options.quick_replies;
    }
    this.send(options, callback);
};

Botly.prototype.sendButtons = function (options, callback) {
    options.payload = this.createButtonTemplate(options.text, options.buttons);
    options.type = ATTACHMENT_TYPE.TEMPLATE;
    this.sendAttachment(options, callback);
};

Botly.prototype.sendGeneric = function (options, callback) {
    options.payload = this.createGenericTemplate(options.elements);
    options.type = ATTACHMENT_TYPE.TEMPLATE;
    this.sendAttachment(options, callback);
};

Botly.prototype.sendReceipt = function (options, callback) {
    options.payload.template_type = TEMPLATE_TYPE.RECEIPT;
    options.type = ATTACHMENT_TYPE.TEMPLATE;
    this.sendAttachment(options, callback);
};

Botly.prototype.createWebURLButton = function (title, url) {
    return {
        type: BUTTON_TYPE.WEB_URL,
        title: title,
        url: url
    };
};

Botly.prototype.createAccountLinkButton = function (url) {
    return {
        type: BUTTON_TYPE.ACCOUNT_LINK,
        url: url
    };
};

Botly.prototype.createPostbackButton = function (title, payload) {
    return {
        type: BUTTON_TYPE.POSTBACK,
        title: title,
        payload: payload
    };
};

Botly.prototype.createQuickReply = function (title, payload) {
    return {
        'content_type': 'text',
        'title': title,
        'payload': payload
    };
};

Botly.prototype.createButtonTemplate = function (text, buttons) {
    if (!Array.isArray(buttons)) {
        buttons = [buttons];
    }
    return {
        template_type: TEMPLATE_TYPE.BUTTON,
        text: text,
        buttons: buttons
    };
};

Botly.prototype.createGenericTemplate = function (elements) {
    if (!Array.isArray(elements)) {
        elements = [elements];
    }
    return {
        template_type: TEMPLATE_TYPE.GENERIC,
        elements: elements
    };
};

Botly.prototype.handleMessage = function (req) {
    const body = _clone(req.body);

    this.emit('complete_message', body);
    body.entry.forEach(entry => {
        entry.messaging.forEach(message => {
            _handleOptIn.call(this, message);
            _handleDelivery.call(this, message);
            _handlePostback.call(this, message);
            _handleAccountLinking.call(this, message);
            _handleUserMessage.call(this, message);
            _handleEcho.call(this, message);
        }, this);
    }, this);
};

function _handleOptIn(message) {
    if (message.optin) {
        let optin = message.optin.ref;
        this.emit('optin', message.sender.id, message, optin);
    }
}

function _handleDelivery(message) {
    if (message.delivery) {
        let mids = message.delivery.mids;
        this.emit('delivery', message.sender.id, message, mids);
    }
}

function _handlePostback(message) {
    if (message.postback || (message.message && !message.message.is_echo && message.message.quick_reply)) {
        let postback = (message.postback && message.postback.payload) || message.message.quick_reply.payload;
        this.emit('postback', message.sender.id, message, postback);
    }
}

function _handleAccountLinking(message) {
    if (message.account_linking) {
        let link = message.account_linking;
        this.emit('account_link', message.sender.id, message, link);
    }
}

function _handleUserMessage(message) {
    var value;
    if (message.message && !message.message.is_echo && message.message.text && !message.message.quick_reply) {
        value = {text: message.message.text};
    }

    if (message.message && !message.message.is_echo && message.message.attachments) {
        let attachments = {};
        message.message.attachments.forEach((attachment) => {
            if (attachment.type && attachment.payload) {
                attachments[attachment.type] = attachments[attachment.type] || [];
                attachments[attachment.type].push(attachment.payload.url || attachment.payload.coordinates);
            }
        });
        value = {attachments: attachments};
    }

    if (value) {
        this.emit('message', message.sender.id, message, value);
    }
}

function _handleEcho(message) {
    if (message.message && message.message.is_echo) {
        this.emit('echo', message.sender.id, message, message.message);
    }
}

function _clone(obj) {
    return JSON.parse(JSON.stringify(obj));
}

Botly.CONST = {
    BUTTON_TYPE: BUTTON_TYPE,
    TEMPLATE_TYPE: TEMPLATE_TYPE,
    ATTACHMENT_TYPE: ATTACHMENT_TYPE,
    NOTIFICATION_TYPE: NOTIFICATION_TYPE,
    ACTION_TYPES: ACTION_TYPES
};

module.exports = Botly;
<|endoftext|>


<commit_before>var config = require('./webpack.config.js');
var BundleTracker = require('webpack-bundle-tracker');
var webpack = require('webpack');

// Built webpack dev asset reloader
config.entry.common.unshift('webpack/hot/dev-server');
// Use 0.0.0.0 so that we can set a port but still use the host
// the browser is connected to.
config.entry.common.unshift('webpack-dev-server/client?http://0.0.0.0:9994');

// Out JS debugging tools
config.entry.common.push('./static/js/debug.js');

config.devtool = 'eval';
config.output.publicPath = '/webpack/';
config.plugins.push(new BundleTracker({filename: 'var/webpack-stats-dev.json'}));
// Hot Reload of code in development
config.plugins.push(new webpack.HotModuleReplacementPlugin());
// Better logging from console for hot reload
config.plugins.push(new webpack.NamedModulesPlugin());

config.devServer = {
    clientLogLevel: "warning",
    hot: true,
    inline: false,
    stats: "errors-only",
    watchOptions: {
        aggregateTimeout: 300,
        poll: 1000,
    },
};

module.exports = config;
<commit_msg>Frontend debug: script-loader should load sourceURL.<commit_after>var config = require('./webpack.config.js');
var BundleTracker = require('webpack-bundle-tracker');
var webpack = require('webpack');

// Built webpack dev asset reloader
config.entry.common.unshift('webpack/hot/dev-server');
// Use 0.0.0.0 so that we can set a port but still use the host
// the browser is connected to.
config.entry.common.unshift('webpack-dev-server/client?http://0.0.0.0:9994');

// Out JS debugging tools
config.entry.common.push('./static/js/debug.js');

config.devtool = 'eval';
config.output.publicPath = '/webpack/';
config.plugins.push(new BundleTracker({filename: 'var/webpack-stats-dev.json'}));
// Hot Reload of code in development
config.plugins.push(new webpack.HotModuleReplacementPlugin());
// Better logging from console for hot reload
config.plugins.push(new webpack.NamedModulesPlugin());

// script-loader should load sourceURL in dev
config.plugins.push(new webpack.LoaderOptionsPlugin({debug: true}));

config.devServer = {
    clientLogLevel: "warning",
    hot: true,
    inline: false,
    stats: "errors-only",
    watchOptions: {
        aggregateTimeout: 300,
        poll: 1000,
    },
};

module.exports = config;
<|endoftext|>


<commit_before>'use strict';

const promiseRetry       = require('promise-retry');
const zlib               = require('zlib');
const del                = require('del');
const nectar             = require('nectar');
const extrakt            = require('extrakt');
const Datastore          = require('nedb');
const pify               = require('pify');
const path               = require('path');
const fs                 = require('fs');
const pathExists         = require('path-exists');
const filesizeParser     = require('filesize-parser');
const defaults           = require('defa');
const _                  = require('lodash');
const EventEmitter       = require('events').EventEmitter;
const Promise            = require('bluebird');
const CacheableOperation = require('./CacheableOperation');
const CachedResult       = require('./CachedResult');

class Cache extends EventEmitter {

    constructor(options) {
        super();
        options = _.isObject(options) ? options : {};
        defaults(options, {
            workspace: path.join(process.cwd(), '.johnny'),
            maxSize:   '512mb'
        }, options => {
            return {
                maxSizeBytes: () => filesizeParser(options.maxSize),
                dataStore:    () => path.join(options.workspace, '.index')
            }
        });
        this.workspace = options.workspace;
        this.maxSize   = options.maxSizeBytes;
        this.index     = new Datastore({filename: options.dataStore, autoload: true});
        this.ready     = this.sync();
    }

    /**
     * Get the relevance score for the given cachedResult (based on this score, irrelevant entries are removed)
     * @param cachedResult
     * @returns {Promise.<Number>}
     */
    getScoreFor(cachedResult) {

        // Substract one point for every month in age
        let age = cachedResult.created / (31 * 24 * 60 * 60 * 1000);

        // Substract one point for every order of magnitude in filesize
        let size = -Math.log10(cachedResult.fileSize);

        // Add two points for every order of magnitude in operation runtime
        let runtime = Math.log10(cachedResult.runtime) * 2;

        // Remove 20 points if there is a newer entry with an equivalent action name in the index
        let redundant = pify(this.index.find.bind(this.index))({
            action:  cachedResult.action,
            created: {$gt: cachedResult.created}
        }).then(docs => docs.length ? -20 : 0);

        let factors = [age, size, runtime, redundant];

        return _.reduce(_.map(factors, score => Promise.resolve(score)), (queue, score) => {
            return queue.then(currentScore => score.then(score => currentScore + score));
        }, Promise.resolve(0));
    }

    /**
     * Makes sure the cache size does not exceed the maximum allowed size, removing the most irrelevant rows from the DB
     * @param {Number} [freeUp = 0] The number of bytes to additionally clear up
     * @returns {Promise}
     */
    maintainMaxSize(freeUp) {
        if (!freeUp) freeUp = 0;
        return this.getAllDocs()
            .then(docs => _.map(docs, doc => CachedResult.fromDocument(null, doc)))
            .then(cachedResults => {
                let totalCacheSize = _.sumBy(cachedResults, cachedResult => cachedResult.fileSize) + freeUp;
                return Promise.map(cachedResults, cachedResult => this.getScoreFor(cachedResult).then(score => {
                    cachedResult.score = score;
                    return cachedResult;
                })).then(cachedResults => {
                    let sortedCacheResults = _.sortBy(cachedResults, 'score');
                    let remove             = [];
                    while (totalCacheSize > this.maxSize) {
                        let removeOne = sortedCacheResults.shift(sortedCacheResults);
                        remove.push(removeOne);
                        totalCacheSize -= removeOne.fileSize;
                    }
                    return remove.length ? this.removeResults(remove) : Promise.resolve();
                })
            });
    }

    /**
     * @param {CachedResult[]} cachedResults
     */
    removeResults(cachedResults) {
        return pify(this.index.remove.bind(this.index))({
            "_id": {$in: _.map(cachedResults, cachedResult => cachedResult._id)}
        });
    }

    /**
     * Returns a promise for the preparation of the cache
     * @returns {Promise}
     */
    awaitReady() {
        return this.ready;
    }

    /**
     * Run some update / refresh operations
     * @returns {Promise}
     */
    sync() {
        return this.untrackExpired()
            .then(() => this.maintainMaxSize())
            .then(() => this.purgeUntracked());
    }

    /**
     * Remove expired row from the index
     * @returns {Promise}
     */
    untrackExpired() {
        return pify(this.index.remove.bind(this.index))({expires: {$ne: -1, $lte: Date.now()}});
    }

    /**
     * Gets all documents from the index
     * @returns {Promise}
     */
    getAllDocs() {
        return pify(this.index.find.bind(this.index))({});
    }

    /**
     * Remove files for which there is no related row in the index
     * @returns {Promise}
     */
    purgeUntracked() {
        return this.getAllDocs()
            .then(docs => [path.join(this.workspace, '*.*')].concat(_.map(docs, doc => {
                return '!' + this.getStorageLocation(CachedResult.fromDocument(null, doc));
            })))
            .then(globs => del(globs));
    }

    /**
     * Prevent hash collisions
     * @param {CachedResult} cachedResult
     * @returns {Promise}
     */
    incrementFilename(cachedResult) {
        let updatedFilename;
        let baseName  = path.basename(cachedResult.fileName).substring(0, path.basename(cachedResult.fileName).lastIndexOf('.'));
        let ext       = path.extname(cachedResult.fileName);
        let increment = 0;
        return promiseRetry((retry) => {
            updatedFilename = baseName + (increment ? ('-' + increment) : '') + ext;
            let fileName    = this.getStorageLocation(updatedFilename);
            return pathExists(fileName).then(exists => {
                if (exists) {
                    increment++;
                    retry(new Error('file exists'));
                }
            });
        }, {retries: 100, minTimeout: 0}).then(() => {
            cachedResult.fileName = updatedFilename;
            return cachedResult;
        });
    }

    /**
     * @param {CachedResult|string} obj
     * @return {string}
     */
    getStorageLocation(obj) {
        if (obj instanceof CachedResult)
            obj = obj.fileName;
        return path.join(this.workspace, obj);
    }

    getExtractPath() {
        return process.cwd();
    }

    /**
     * Creates a new CachedResult object for saving
     * @param cacheableOperation
     * @returns {Promise.<CachedResult>}
     */
    prepareResult(cacheableOperation) {
        const cachedResult = CachedResult.createNew(cacheableOperation);
        return cachedResult.setHashes(cacheableOperation)
            .then(cachedResult => this.incrementFilename(cachedResult));
    }

    /**
     * @param {CacheableOperation} cacheableOperation
     * @return {Promise.<CachedResult>}
     */
    run(cacheableOperation) {
        let cachedResult;
        return this.awaitReady()
            .then(() => Promise.all([this.prepareResult(cacheableOperation), cacheableOperation.run()]))
            .then(values => {
                cachedResult = values[0];
                if (cacheableOperation.onStore) cacheableOperation.onStore(cacheableOperation);
                let writeStream = fs.createWriteStream(this.getStorageLocation(cachedResult));
                if (cacheableOperation.compress) writeStream = zlib.createGzip().pipe(writeStream);
                return nectar(cacheableOperation.output, writeStream);
            })
            .then(() => this.assignFileSize(cachedResult))
            .then(() => pify(this.index.insert.bind(this.index))(cachedResult.getDocument()))
            .then(() => this.sync())
            .then(() => cachedResult);
    }

    /**
     * Restores the given cached result
     * @param cachedResult
     * @returns {Promise.<CachedResult>}
     */
    restore(cachedResult) {
        return extrakt(this.getStorageLocation(cachedResult), this.getExtractPath())
            .then(() => cachedResult);
    }

    /**
     * Generates the query for retreiving the appropriate cached result document
     * @param {CacheableOperation} cacheableOperation
     * @returns {Promise.<object>}
     */
    getQuery(cacheableOperation) {
        return Promise.all([
                cacheableOperation.getFileHash(),
                cacheableOperation.getOutputHash()
            ])
            .then(hashes => {
                return {
                    fileHash:   hashes[0],
                    outputHash: hashes[1],
                    action:     cacheableOperation.action,
                    $or:        [{expires: -1}, {expires: {$gt: Date.now()}}]
                };
            });
    }

    /**
     * @param {CacheableOperation} cacheableOperation
     * @returns {Promise.<object|null>}
     */
    getCachedResult(cacheableOperation) {
        let jobs = [this.getQuery(cacheableOperation), this.awaitReady()];
        return Promise.all(jobs)
            .then(values => pify(this.index.find.bind(this.index))(values[0]))
            .then(docs => docs.length ? CachedResult.fromDocument(cacheableOperation, docs[0]) : null);
    }

    /**
     * Check the file size of the storage file related to the cached result and assign it to the object
     * @param {CachedResult} cachedResult
     * @returns {Promise}
     */
    assignFileSize(cachedResult) {
        return pify(fs.stat)(this.getStorageLocation(cachedResult))
            .then(stat => {
                cachedResult.fileSize = stat.size;
            }, () => null);
    }

    /**
     * Checks whether the cached result has a corresponding file
     * @param {CachedResult} cachedResult
     * @returns {Promise.<CachedResult|null>}
     */
    ensureExists(cachedResult) {
        return pify(fs.access)(this.getStorageLocation(cachedResult))
            .then(() => cachedResult, () => null);
    }

    /**
     * @param {function} run
     * @param {object} options
     * @returns {Promise.<CachedResult>}
     */
    doCached(run, options) {
        let cacheableOperation = new CacheableOperation(run, options);
        return this.getCachedResult(cacheableOperation)
            .then(cachedResult => cachedResult !== null ? this.ensureExists(cachedResult) : null)
            .then(cachedResult => {
                if (cachedResult !== null) {
                    if (options.onRestore) options.onRestore(cacheableOperation, cachedResult);
                    return this.restore(cachedResult);
                } else {
                    if (options.onRun) options.onRun(cacheableOperation);
                    return this.run(cacheableOperation);
                }
            });
    }
}

module.exports = Cache;<commit_msg>Run prepareResult in series after op.run<commit_after>'use strict';

const promiseRetry       = require('promise-retry');
const zlib               = require('zlib');
const del                = require('del');
const nectar             = require('nectar');
const extrakt            = require('extrakt');
const Datastore          = require('nedb');
const pify               = require('pify');
const path               = require('path');
const fs                 = require('fs');
const pathExists         = require('path-exists');
const filesizeParser     = require('filesize-parser');
const defaults           = require('defa');
const _                  = require('lodash');
const EventEmitter       = require('events').EventEmitter;
const Promise            = require('bluebird');
const CacheableOperation = require('./CacheableOperation');
const CachedResult       = require('./CachedResult');

class Cache extends EventEmitter {

    constructor(options) {
        super();
        options = _.isObject(options) ? options : {};
        defaults(options, {
            workspace: path.join(process.cwd(), '.johnny'),
            maxSize:   '512mb'
        }, options => {
            return {
                maxSizeBytes: () => filesizeParser(options.maxSize),
                dataStore:    () => path.join(options.workspace, '.index')
            }
        });
        this.workspace = options.workspace;
        this.maxSize   = options.maxSizeBytes;
        this.index     = new Datastore({filename: options.dataStore, autoload: true});
        this.ready     = this.sync();
    }

    /**
     * Get the relevance score for the given cachedResult (based on this score, irrelevant entries are removed)
     * @param cachedResult
     * @returns {Promise.<Number>}
     */
    getScoreFor(cachedResult) {

        // Substract one point for every month in age
        let age = cachedResult.created / (31 * 24 * 60 * 60 * 1000);

        // Substract one point for every order of magnitude in filesize
        let size = -Math.log10(cachedResult.fileSize);

        // Add two points for every order of magnitude in operation runtime
        let runtime = Math.log10(cachedResult.runtime) * 2;

        // Remove 20 points if there is a newer entry with an equivalent action name in the index
        let redundant = pify(this.index.find.bind(this.index))({
            action:  cachedResult.action,
            created: {$gt: cachedResult.created}
        }).then(docs => docs.length ? -20 : 0);

        let factors = [age, size, runtime, redundant];

        return _.reduce(_.map(factors, score => Promise.resolve(score)), (queue, score) => {
            return queue.then(currentScore => score.then(score => currentScore + score));
        }, Promise.resolve(0));
    }

    /**
     * Makes sure the cache size does not exceed the maximum allowed size, removing the most irrelevant rows from the DB
     * @param {Number} [freeUp = 0] The number of bytes to additionally clear up
     * @returns {Promise}
     */
    maintainMaxSize(freeUp) {
        if (!freeUp) freeUp = 0;
        return this.getAllDocs()
            .then(docs => _.map(docs, doc => CachedResult.fromDocument(null, doc)))
            .then(cachedResults => {
                let totalCacheSize = _.sumBy(cachedResults, cachedResult => cachedResult.fileSize) + freeUp;
                return Promise.map(cachedResults, cachedResult => this.getScoreFor(cachedResult).then(score => {
                    cachedResult.score = score;
                    return cachedResult;
                })).then(cachedResults => {
                    let sortedCacheResults = _.sortBy(cachedResults, 'score');
                    let remove             = [];
                    while (totalCacheSize > this.maxSize) {
                        let removeOne = sortedCacheResults.shift(sortedCacheResults);
                        remove.push(removeOne);
                        totalCacheSize -= removeOne.fileSize;
                    }
                    return remove.length ? this.removeResults(remove) : Promise.resolve();
                })
            });
    }

    /**
     * @param {CachedResult[]} cachedResults
     */
    removeResults(cachedResults) {
        return pify(this.index.remove.bind(this.index))({
            "_id": {$in: _.map(cachedResults, cachedResult => cachedResult._id)}
        });
    }

    /**
     * Returns a promise for the preparation of the cache
     * @returns {Promise}
     */
    awaitReady() {
        return this.ready;
    }

    /**
     * Run some update / refresh operations
     * @returns {Promise}
     */
    sync() {
        return this.untrackExpired()
            .then(() => this.maintainMaxSize())
            .then(() => this.purgeUntracked());
    }

    /**
     * Remove expired row from the index
     * @returns {Promise}
     */
    untrackExpired() {
        return pify(this.index.remove.bind(this.index))({expires: {$ne: -1, $lte: Date.now()}});
    }

    /**
     * Gets all documents from the index
     * @returns {Promise}
     */
    getAllDocs() {
        return pify(this.index.find.bind(this.index))({});
    }

    /**
     * Remove files for which there is no related row in the index
     * @returns {Promise}
     */
    purgeUntracked() {
        return this.getAllDocs()
            .then(docs => [path.join(this.workspace, '*.*')].concat(_.map(docs, doc => {
                return '!' + this.getStorageLocation(CachedResult.fromDocument(null, doc));
            })))
            .then(globs => del(globs));
    }

    /**
     * Prevent hash collisions
     * @param {CachedResult} cachedResult
     * @returns {Promise}
     */
    incrementFilename(cachedResult) {
        let updatedFilename;
        let baseName  = path.basename(cachedResult.fileName).substring(0, path.basename(cachedResult.fileName).lastIndexOf('.'));
        let ext       = path.extname(cachedResult.fileName);
        let increment = 0;
        return promiseRetry((retry) => {
            updatedFilename = baseName + (increment ? ('-' + increment) : '') + ext;
            let fileName    = this.getStorageLocation(updatedFilename);
            return pathExists(fileName).then(exists => {
                if (exists) {
                    increment++;
                    retry(new Error('file exists'));
                }
            });
        }, {retries: 100, minTimeout: 0}).then(() => {
            cachedResult.fileName = updatedFilename;
            return cachedResult;
        });
    }

    /**
     * @param {CachedResult|string} obj
     * @return {string}
     */
    getStorageLocation(obj) {
        if (obj instanceof CachedResult)
            obj = obj.fileName;
        return path.join(this.workspace, obj);
    }

    getExtractPath() {
        return process.cwd();
    }

    /**
     * Creates a new CachedResult object for saving
     * @param cacheableOperation
     * @returns {Promise.<CachedResult>}
     */
    prepareResult(cacheableOperation) {
        const cachedResult = CachedResult.createNew(cacheableOperation);
        return cachedResult.setHashes(cacheableOperation)
            .then(cachedResult => this.incrementFilename(cachedResult));
    }

    /**
     * @param {CacheableOperation} cacheableOperation
     * @return {Promise.<CachedResult>}
     */
    run(cacheableOperation) {
        let cachedResult;
        return this.awaitReady()
            .then(() => cacheableOperation.run())
            .then(() => this.prepareResult(cacheableOperation))
            .then(result => {
                cachedResult = result;
                if (cacheableOperation.onStore) cacheableOperation.onStore(cacheableOperation);
                let writeStream = fs.createWriteStream(this.getStorageLocation(cachedResult));
                if (cacheableOperation.compress) writeStream = zlib.createGzip().pipe(writeStream);
                return nectar(cacheableOperation.output, writeStream);
            })
            .then(() => this.assignFileSize(cachedResult))
            .then(() => pify(this.index.insert.bind(this.index))(cachedResult.getDocument()))
            .then(() => this.sync())
            .then(() => cachedResult);
    }

    /**
     * Restores the given cached result
     * @param cachedResult
     * @returns {Promise.<CachedResult>}
     */
    restore(cachedResult) {
        return extrakt(this.getStorageLocation(cachedResult), this.getExtractPath())
            .then(() => cachedResult);
    }

    /**
     * Generates the query for retreiving the appropriate cached result document
     * @param {CacheableOperation} cacheableOperation
     * @returns {Promise.<object>}
     */
    getQuery(cacheableOperation) {
        return Promise.all([
                cacheableOperation.getFileHash(),
                cacheableOperation.getOutputHash()
            ])
            .then(hashes => {
                return {
                    fileHash:   hashes[0],
                    outputHash: hashes[1],
                    action:     cacheableOperation.action,
                    $or:        [{expires: -1}, {expires: {$gt: Date.now()}}]
                };
            });
    }

    /**
     * @param {CacheableOperation} cacheableOperation
     * @returns {Promise.<object|null>}
     */
    getCachedResult(cacheableOperation) {
        let jobs = [this.getQuery(cacheableOperation), this.awaitReady()];
        return Promise.all(jobs)
            .then(values => pify(this.index.find.bind(this.index))(values[0]))
            .then(docs => docs.length ? CachedResult.fromDocument(cacheableOperation, docs[0]) : null);
    }

    /**
     * Check the file size of the storage file related to the cached result and assign it to the object
     * @param {CachedResult} cachedResult
     * @returns {Promise}
     */
    assignFileSize(cachedResult) {
        return pify(fs.stat)(this.getStorageLocation(cachedResult))
            .then(stat => {
                cachedResult.fileSize = stat.size;
            }, () => null);
    }

    /**
     * Checks whether the cached result has a corresponding file
     * @param {CachedResult} cachedResult
     * @returns {Promise.<CachedResult|null>}
     */
    ensureExists(cachedResult) {
        return pify(fs.access)(this.getStorageLocation(cachedResult))
            .then(() => cachedResult, () => null);
    }

    /**
     * @param {function} run
     * @param {object} options
     * @returns {Promise.<CachedResult>}
     */
    doCached(run, options) {
        let cacheableOperation = new CacheableOperation(run, options);
        return this.getCachedResult(cacheableOperation)
            .then(cachedResult => cachedResult !== null ? this.ensureExists(cachedResult) : null)
            .then(cachedResult => {
                if (cachedResult !== null) {
                    if (options.onRestore) options.onRestore(cacheableOperation, cachedResult);
                    return this.restore(cachedResult);
                } else {
                    if (options.onRun) options.onRun(cacheableOperation);
                    return this.run(cacheableOperation);
                }
            });
    }
}

module.exports = Cache;<|endoftext|>