<commit_before>/**
 * search index admin. console
 * @author adrienjoly, whyd
 **/

var mongodb = require("../../models/mongodb.js");
var searchModel = require("../../models/search.js");

var indexCol = {
	user: "user",
	post: "post",
	playlist: "user"
};

var indexFields = { // different from the one from search.js model
	user: {_id:1, name:1, email:1, handle:1},
	post: {_id:1, name:1, text:1, uId:1},
	playlist: {_id:1, name:1, pl:1}
};

function deleteIndex(type, cb) {
	console.log("Deleting "+type+"s from index ...");
	searchModel.deleteAllDocs(type, function(r) {
		console.log("Deleting "+type+"s from index: DONE!");
		if (cb)
			cb(r);
	});
}

function refreshIndex(type, cb, preprocess) {
	var BULK_SIZE = 1000, bulkDocs = [], fetched = 0, indexed = 0;
	console.log("Indexing "+type+"s ...");
	function flush(cb){
		searchModel.indexBulk(bulkDocs, function(r){
			r = r || {};
			if (r.errors || r.error)
				console.error("[ERR] (BULK) admin.index.refreshIndex, searchModel.index: " + (r.errors || r.error));
			indexed += (r.items || []).length || 0;
			cb();
		});
		bulkDocs = [];
	}
	function index(doc, cb){
		if (doc) {
			doc._type = doc._type || type;
			//searchModel.index(doc, cb);
			bulkDocs.push(doc);
		}
		if (bulkDocs.length >= BULK_SIZE)
			flush(cb);
		else
			cb();
	}
	var process = !preprocess ? index : function(obj, fetchAndProcessNextObject) {
		preprocess(obj, fetchAndProcessNextObject, index);
	};
	var options = {
		fields: indexFields[type],
		//limit: 9999999,
		sort:[['_id','desc']],
		batchSize: 1000
	};
	console.log("index: iterating on collection:", indexCol[type]);
	mongodb.collections[indexCol[type]].find({}, options, function(err, cursor) {
		(function next() {
			cursor.nextObject(function(err, u) {
				if (err)
					console.log("[ERR] admin.index.refreshIndex, db.nextObject: ", err)
				if (u != null) {
					++fetched;
					process(u, function(){
						if (fetched % 100 == 0)
							console.warn("=> last (BULK) indexed document: ", u._id);
						setTimeout(next /*, 100*/);
					});
				}
				else {
					flush(function(){
						console.log("admin.index.refreshIndex DONE! => indexed", indexed, "documents from", fetched, 'fetched db records');
						cb && cb();
					});
				}
			});
		})();
	});
}

var indexFcts = {
	deleteUserIndex: function(cb) {
		deleteIndex("user", cb);
	},
	deletePostIndex: function(cb) {
		deleteIndex("post", cb);
	},
	deletePlaylistIndex: function(cb) {
		deleteIndex("playlist", cb);
	},
	refreshUserIndex: function(cb) {
		refreshIndex("user", cb);
	},
	refreshPostIndex: function(cb) {
		refreshIndex("post", cb);
	},
	refreshPlaylistIndex: function(cb) {
		refreshIndex("playlist", cb, function(user, nextUser, index){
			function nextPlaylist() {
				var p = user.pl.pop();
				if (!p)
					nextUser(); // no more playlists for this user => process next user
				else {
					p._id = ""+user._id + "_" + p.id;
					delete p.id;
					index(p, nextPlaylist);
				}
			}
			if (!user || !user.pl || !user.pl.length)
				nextUser();
			else
				nextPlaylist();
		});
	}
}

function countDbUsersAndPlaylists(cb){
	var result = {
		dbUsers: 0,
		dbPlaylists: 0
	};
	mongodb.collections[indexCol["user"]].find({}, {fields:{pl:1}}, function(err, cursor){
		(function nextUser(){
			cursor.nextObject(function(err, user){
				if (!user)
					cb(result);
				else {
					++result.dbUsers;
					if (user.pl)
						result.dbPlaylists += user.pl.length;
					setImmediate(nextUser);
				}
			});
		})();
	});
}

function countItems(cb) {
	countDbUsersAndPlaylists(function(p){
		mongodb.collections[indexCol["post"]].count(function(err, dbPosts){
			p.dbPosts = dbPosts;
			searchModel.countDocs("user", function(idxUsers){
				p.idxUsers = idxUsers
				searchModel.countDocs("post", function(idxPosts){
					p.idxPosts = idxPosts;
					searchModel.countDocs("playlist", function(idxPlaylists){
						p.idxPlaylists = idxPlaylists;
						/*
						p = {
							idxUsers: idxUsers,
							idxPosts: idxPosts,
							idxPlaylists: idxPlaylists,
							dbUsers: dbUsers,
							dbPosts: dbPosts,
							dbPlaylists: dbPlaylists
						};*/
						cb(p);
					});
				});
			});
		});
	});
}

function renderForm(p){
	return [
	//	'
' + (p.message || '') + '
',
		'<form method="post">',
			'
User index ( '+p.idxUsers+' / '+p.dbUsers+' )',
				'<input type="submit" name="deleteUserIndex" value="DELETE">',
				'<input type="submit" name="refreshUserIndex" value="refresh">',
				'
',
			'
Post index ( '+p.idxPosts+' / '+p.dbPosts+' )',
				'<input type="submit" name="deletePostIndex" value="DELETE">',
				'<input type="submit" name="refreshPostIndex" value="refresh">',
				'
',
			'
Playlist index ( '+p.idxPlaylists+' / '+p.dbPlaylists+' )',
				'<input type="submit" name="deletePlaylistIndex" value="DELETE">',
				'<input type="submit" name="refreshPlaylistIndex" value="refresh">',
				'
',
		'</form>'
	].join('\n');
}

exports.controller = function(request, reqParams, response, error) {
	request.logToConsole("admin.index.controller", request.body || reqParams);
	var reqParams = reqParams || {};

	// make sure an admin is logged, or return an error page
	reqParams.loggedUser = request.checkAdmin(response);
	if (!reqParams.loggedUser) return;

	if (request.method.toLowerCase() === 'post') {
		for (var i in indexFcts)
			if (request.body[i])
				return indexFcts[i](function(r) {
					response.render(r || {ok:"done"});
				});
		response.badRequest();
	}
	else
		countItems(function(p){
			p.message = reqParams.message;
			response.renderHTML(renderForm(p));
		});
}
<commit_msg>bulk refresh also index: - eId and pl fields on posts - uId field on playlists<commit_after>/**
 * search index admin. console
 * @author adrienjoly, whyd
 **/

var mongodb = require("../../models/mongodb.js");
var searchModel = require("../../models/search.js");

var indexCol = {
	user: "user",
	post: "post",
	playlist: "user"
};

var indexFields = { // different from the one from search.js model
	user: {_id:1, name:1, email:1, handle:1},
	post: {_id:1, name:1, text:1, uId:1, eId:1, pl:1},
	playlist: {_id:1, name:1, pl:1, uId:1},
};

function deleteIndex(type, cb) {
	console.log("Deleting "+type+"s from index ...");
	searchModel.deleteAllDocs(type, function(r) {
		console.log("Deleting "+type+"s from index: DONE!");
		if (cb)
			cb(r);
	});
}

function refreshIndex(type, cb, preprocess) {
	var BULK_SIZE = 1000, bulkDocs = [], fetched = 0, indexed = 0;
	console.log("Indexing "+type+"s ...");
	function flush(cb){
		searchModel.indexBulk(bulkDocs, function(r){
			r = r || {};
			if (r.errors || r.error)
				console.error("[ERR] (BULK) admin.index.refreshIndex, searchModel.index: " + (r.errors || r.error));
			indexed += (r.items || []).length || 0;
			cb();
		});
		bulkDocs = [];
	}
	function index(doc, cb){
		if (doc) {
			doc._type = doc._type || type;
			//searchModel.index(doc, cb);
			bulkDocs.push(doc);
		}
		if (bulkDocs.length >= BULK_SIZE)
			flush(cb);
		else
			cb();
	}
	var process = !preprocess ? index : function(obj, fetchAndProcessNextObject) {
		preprocess(obj, fetchAndProcessNextObject, index);
	};
	var options = {
		fields: indexFields[type],
		//limit: 9999999,
		sort:[['_id','desc']],
		batchSize: 1000
	};
	console.log("index: iterating on collection:", indexCol[type]);
	mongodb.collections[indexCol[type]].find({}, options, function(err, cursor) {
		(function next() {
			cursor.nextObject(function(err, u) {
				if (err)
					console.log("[ERR] admin.index.refreshIndex, db.nextObject: ", err)
				if (u != null) {
					++fetched;
					process(u, function(){
						if (fetched % 100 == 0)
							console.warn("=> last (BULK) indexed document: ", u._id);
						setTimeout(next /*, 100*/);
					});
				}
				else {
					flush(function(){
						console.log("admin.index.refreshIndex DONE! => indexed", indexed, "documents from", fetched, 'fetched db records');
						cb && cb();
					});
				}
			});
		})();
	});
}

var indexFcts = {
	deleteUserIndex: function(cb) {
		deleteIndex("user", cb);
	},
	deletePostIndex: function(cb) {
		deleteIndex("post", cb);
	},
	deletePlaylistIndex: function(cb) {
		deleteIndex("playlist", cb);
	},
	refreshUserIndex: function(cb) {
		refreshIndex("user", cb);
	},
	refreshPostIndex: function(cb) {
		refreshIndex("post", cb);
	},
	refreshPlaylistIndex: function(cb) {
		refreshIndex("playlist", cb, function(user, nextUser, index){
			function nextPlaylist() {
				var p = user.pl.pop();
				if (!p)
					nextUser(); // no more playlists for this user => process next user
				else {
					p._id = ""+user._id + "_" + p.id;
					delete p.id;
					index(p, nextPlaylist);
				}
			}
			if (!user || !user.pl || !user.pl.length)
				nextUser();
			else
				nextPlaylist();
		});
	}
}

function countDbUsersAndPlaylists(cb){
	var result = {
		dbUsers: 0,
		dbPlaylists: 0
	};
	mongodb.collections[indexCol["user"]].find({}, {fields:{pl:1}}, function(err, cursor){
		(function nextUser(){
			cursor.nextObject(function(err, user){
				if (!user)
					cb(result);
				else {
					++result.dbUsers;
					if (user.pl)
						result.dbPlaylists += user.pl.length;
					setImmediate(nextUser);
				}
			});
		})();
	});
}

function countItems(cb) {
	countDbUsersAndPlaylists(function(p){
		mongodb.collections[indexCol["post"]].count(function(err, dbPosts){
			p.dbPosts = dbPosts;
			searchModel.countDocs("user", function(idxUsers){
				p.idxUsers = idxUsers
				searchModel.countDocs("post", function(idxPosts){
					p.idxPosts = idxPosts;
					searchModel.countDocs("playlist", function(idxPlaylists){
						p.idxPlaylists = idxPlaylists;
						/*
						p = {
							idxUsers: idxUsers,
							idxPosts: idxPosts,
							idxPlaylists: idxPlaylists,
							dbUsers: dbUsers,
							dbPosts: dbPosts,
							dbPlaylists: dbPlaylists
						};*/
						cb(p);
					});
				});
			});
		});
	});
}

function renderForm(p){
	return [
	//	'
' + (p.message || '') + '
',
		'<form method="post">',
			'
User index ( '+p.idxUsers+' / '+p.dbUsers+' )',
				'<input type="submit" name="deleteUserIndex" value="DELETE">',
				'<input type="submit" name="refreshUserIndex" value="refresh">',
				'
',
			'
Post index ( '+p.idxPosts+' / '+p.dbPosts+' )',
				'<input type="submit" name="deletePostIndex" value="DELETE">',
				'<input type="submit" name="refreshPostIndex" value="refresh">',
				'
',
			'
Playlist index ( '+p.idxPlaylists+' / '+p.dbPlaylists+' )',
				'<input type="submit" name="deletePlaylistIndex" value="DELETE">',
				'<input type="submit" name="refreshPlaylistIndex" value="refresh">',
				'
',
		'</form>'
	].join('\n');
}

exports.controller = function(request, reqParams, response, error) {
	request.logToConsole("admin.index.controller", request.body || reqParams);
	var reqParams = reqParams || {};

	// make sure an admin is logged, or return an error page
	reqParams.loggedUser = request.checkAdmin(response);
	if (!reqParams.loggedUser) return;

	if (request.method.toLowerCase() === 'post') {
		for (var i in indexFcts)
			if (request.body[i])
				return indexFcts[i](function(r) {
					response.render(r || {ok:"done"});
				});
		response.badRequest();
	}
	else
		countItems(function(p){
			p.message = reqParams.message;
			response.renderHTML(renderForm(p));
		});
}
<|endoftext|>


<commit_before>var g_e;
define(['jquery.drag'], function(drag) {

    function Controller(network) {
        var self = this;
        this.network = network;
        this.mouseDown = false;

        $(function() {

            var elem = $('.controller');

            /*
            elem.bind('mousedown', function(ev, dd) {
                console.log('dragstart');
                self.lastX = 0;
                self.lastY = 0;
            });
            */

            elem.bind('mousedown', function() {
                console.log('start');
                network.tabletEvent('start', {x:0, y:0});
                $(document).trigger('clear_canvas');
            });

            if('ontouchstart' in document.documentElement) document.getElementById('controller').addEventListener('touchstart', function() {
                console.log('touchstart');
                $(document).trigger('clear_canvas');
                network.tabletEvent('start', {x:0, y:0});
            }, false);

            elem.drag(function(ev, dd) {
                console.log('deltaX : ' + dd.deltaX + ' offsetX : ' + dd.offsetX);
                network.tabletEvent('move', {x:dd.deltaX, y:dd.deltaY});
            });

            var toggler = $('#toggler');
            toggler.bind('click', function() {
                elem.toggle();
                if(elem.is(':visible')) {
                    console.log('toggler white');
                    toggler.css('background', 'white');
                } else {
                    console.log('toggler none');
                    toggler.css('background-color', 'rgba(0,0,0,0)');
                }
            });

        });
    }

    return Controller;

});
<commit_msg>rmving commented out code<commit_after>var g_e;
define(['jquery.drag'], function(drag) {

    function Controller(network) {
        var self = this;
        this.network = network;
        this.mouseDown = false;

        $(function() {

            var elem = $('.controller');

            elem.bind('mousedown', function() {
                console.log('start');
                network.tabletEvent('start', {x:0, y:0});
                $(document).trigger('clear_canvas');
            });

            if('ontouchstart' in document.documentElement) document.getElementById('controller').addEventListener('touchstart', function() {
                console.log('touchstart');
                $(document).trigger('clear_canvas');
                network.tabletEvent('start', {x:0, y:0});
            }, false);

            elem.drag(function(ev, dd) {
                console.log('deltaX : ' + dd.deltaX + ' offsetX : ' + dd.offsetX);
                network.tabletEvent('move', {x:dd.deltaX, y:dd.deltaY});
            });

            var toggler = $('#toggler');
            toggler.bind('click', function() {
                elem.toggle();
                if(elem.is(':visible')) {
                    console.log('toggler white');
                    toggler.css('background', 'white');
                } else {
                    console.log('toggler none');
                    toggler.css('background-color', 'rgba(0,0,0,0)');
                }
            });

        });
    }

    return Controller;

});
<|endoftext|>


<commit_before>// A comma separated list of currencies to display.
var ticker_currencies = "USD,EUR,JPY,CNY";

function ticker(currencies) {
  var symbols = {
    USD: "$",
    CNY: "¥",
    JPY: "¥",
    EUR: "€"
  };

  $.ajax({
    type: "GET",
    url: "https://min-api.cryptocompare.com/data/price?fsym=BCH&tsyms=" + currencies,
    contentType: "application/json; charset=utf-8",
    timeout: 6000,
    error: function (x, t, m) {
      if ($('#ticker_value').html() === 'Loading...') {
        $('#ticker_value').html("N/A");
      }
    },
    success: function (currencyRates) {
      var output = [];

      $.each(currencyRates, function (currency, price) {
        var sym = symbols[currency];

        if (sym === undefined) {
          return;
        }

        output.push("BCH/" + currency + " " + sym + price);
      });

      if (output.length > 0) {
        $('#ticker_value').html(output.join(" • "));
        return;
      }

      if ($('#ticker_value').html() === 'Loading...') {
        $('#ticker_value').html("N/A");
      }
    }
  }).done(function () {
    setTimeout(function(){ ticker(ticker_currencies); }, 10000);
  }).fail(function() {
    setTimeout(function(){ ticker(ticker_currencies); }, 10000);
  });
}

ticker(ticker_currencies);<commit_msg>Only fetch price updates every 30 seconds<commit_after>// A comma separated list of currencies to display.
var ticker_currencies = "USD,EUR,JPY,CNY";

// Update interval in ms.
var update_interval = 30000;

function ticker(currencies) {
  var symbols = {
    USD: "$",
    CNY: "¥",
    JPY: "¥",
    EUR: "€"
  };

  $.ajax({
    type: "GET",
    url: "https://min-api.cryptocompare.com/data/price?fsym=BCH&tsyms=" + currencies,
    contentType: "application/json; charset=utf-8",
    timeout: 6000,
    error: function (x, t, m) {
      if ($('#ticker_value').html() === 'Loading...') {
        $('#ticker_value').html("N/A");
      }
    },
    success: function (currencyRates) {
      var output = [];

      $.each(currencyRates, function (currency, price) {
        var sym = symbols[currency];

        if (sym === undefined) {
          return;
        }

        output.push("BCH/" + currency + " " + sym + price);
      });

      if (output.length > 0) {
        $('#ticker_value').html(output.join(" • "));
        return;
      }

      if ($('#ticker_value').html() === 'Loading...') {
        $('#ticker_value').html("N/A");
      }
    }
  }).done(function () {
    setTimeout(function(){ ticker(ticker_currencies); }, update_interval);
  }).fail(function() {
    setTimeout(function(){ ticker(ticker_currencies); }, update_interval);
  });
}

ticker(ticker_currencies);<|endoftext|>


<commit_before>const fixtureJsonStringify = require(`./fixture-json-stringify.js`);
const { getObjectSortedByKeys } = require(`./register.js`);

require(`./load-env-file.js`);

const { Octokit } = require(`@octokit/rest`);

/** @typedef {import('./types.js').FixtureCreateResult} FixtureCreateResult */

/**
 * @param {FixtureCreateResult} fixtureCreateResult The object like returned by an import plugin.
 * @param {String|null} [githubUsername=null] GitHub user name of the fixture submitter.
 * @param {String|null} [githubComment=null] Additional comment for the pull request.
 * @returns {Promise.<String, Error>} A promise that resolves to the pull request URL, or rejects with an error.
 */
module.exports = async function createPullRequest(fixtureCreateResult, githubUsername = null, githubComment = null) {
  const {
    manufacturers,
    fixtures,
    warnings,
    errors,
  } = fixtureCreateResult;

  const repository = process.env.NODE_ENV === `production` ? `open-fixture-library` : `ofl-test`;

  const userToken = process.env.GITHUB_USER_TOKEN;
  if (userToken === undefined) {
    console.error(`.env file does not contain GITHUB_USER_TOKEN variable`);
    throw new Error(`GitHub user token was not set`);
  }

  const timestamp = new Date().toISOString().replace(/:/g, `-`).replace(/\..+/, ``);
  const branchName = `branch${timestamp}`;

  const changedFiles = [];
  const githubErrors = [];

  const githubClient = new Octokit({
    auth: `token ${userToken}`,
  });

  try {
    console.log(`get latest commit hash ...`);
    const latestCommitHash = (await githubClient.git.getRef({
      owner: `OpenLightingProject`,
      repo: repository,
      ref: `heads/master`,
    })).data.object.sha;
    console.log(latestCommitHash);

    console.log(`create new branch '${branchName}' ...`);
    await githubClient.git.createRef({
      owner: `OpenLightingProject`,
      repo: repository,
      ref: `refs/heads/${branchName}`,
      sha: latestCommitHash,
    });
    console.log(`done`);

    const user = await getGithubUserInfo();

    const skipCi = Object.keys(fixtures).length > 0;
    await addOrUpdateFile(`fixtures/manufacturers.json`, `manufacturers.json`, oldFileContent => {
      const mergedManufacturers = Object.assign(
        oldFileContent ? JSON.parse(oldFileContent) : {
          $schema: `https://raw.githubusercontent.com/OpenLightingProject/open-fixture-library/master/schemas/manufacturers.json`,
        },
        manufacturers,
      );

      return prettyJsonStringify(getObjectSortedByKeys(mergedManufacturers));
    }, user, skipCi);

    const fixtureEntries = Object.entries(fixtures);
    let index = 0;
    for (const [fixtureKey, fixture] of fixtureEntries) {
      await addOrUpdateFile(
        `fixtures/${fixtureKey}.json`,
        `fixture '${fixtureKey}'`,
        oldFileContent => fixtureJsonStringify(fixture),
        user,
        index !== fixtureEntries.length - 1, // all but the last fixture should skip CI
      );
      index++;
    }

    console.log(`create pull request ...`);

    const addedFixtures = changedFiles.filter(line => line.startsWith(`Add fixture`));
    let title = `Add ${addedFixtures.length} new fixtures`;
    if (addedFixtures.length === 1) {
      title = addedFixtures[0];
    }

    const result = await githubClient.pulls.create({
      owner: `OpenLightingProject`,
      repo: repository,
      title: title,
      body: getPrDescriptionMarkdown(user),
      head: branchName,
      base: `master`,
    });
    console.log(`done`);

    console.log(`add labels to pull request ...`);
    await githubClient.issues.addLabels({
      owner: `OpenLightingProject`,
      repo: repository,
      'issue_number': result.data.number,
      labels: [`new-fixture`, `via-editor`],
    });
    console.log(`done`);

    const pullRequestUrl = result.data.html_url;
    console.log(`View the pull request at ${pullRequestUrl}`);

    return pullRequestUrl;
  }
  catch (error) {
    console.error(`Error: ${error.message}`);
    throw error;
  }

  /**
   * @returns {Object|null} GitHub user information about the submitter, or null if it is not a valid GitHub user.
   */
  async function getGithubUserInfo() {
    if (!githubUsername) {
      return null;
    }

    console.log(`get info for GitHub user @${githubUsername} ...`);

    try {
      const user = (await githubClient.users.getByUsername({
        username: githubUsername,
      })).data;

      console.log(`done (name: ${user.name}, email: ${user.email})`);
      return user;
    }
    catch (error) {
      console.log(`error getting user`);
      return null;
    }
  }

  /**
   * @param {Object|null} user GitHub user information about the submitter, or null if they are not a GitHub user.
   * @returns {String} The pull request description body markdown.
   */
  function getPrDescriptionMarkdown(user) {
    let body = changedFiles.map(line => `* ${line}`).join(`\n`);

    if (githubErrors.length > 0) {
      body += `\n\n### Errors:\n* :warning: ${githubErrors.join(`\n* :warning: `)}`;
    }

    const fixtureWarningsErrors = getFixtureWarningsErrorsMarkdownList();
    if (fixtureWarningsErrors.length) {
      body += `\n\n### Fixture warnings / errors\n\n`;
      body += fixtureWarningsErrors;
    }

    if (githubComment) {
      body += `\n\n### User comment\n\n`;
      body += githubComment;
    }

    body += `\n\nThank you ${getSubmitterNameMarkdown(user)}!`;

    return body;
  }

  /**
   * @returns {String} A Markdown list of all fixture warnings and errors.
   */
  function getFixtureWarningsErrorsMarkdownList() {
    let markdownList = ``;

    for (const fixtureKey of Object.keys(fixtures)) {
      const fixtureErrors = errors[fixtureKey] || [];
      const fixtureWarnings = warnings[fixtureKey] || [];

      const messages = fixtureErrors.map(
        error => `  - :x: ${error}`,
      ).concat(fixtureWarnings.map(
        warning => `  - :warning: ${warning}`,
      ));

      if (messages.length) {
        markdownList += `* ${fixtureKey}\n${messages.join(`\n`)}\n`;
      }
    }

    return markdownList;
  }

  /**
   * @param {Object|null} user GitHub user information about the submitter, or null if they are not a GitHub user.
   * @returns {String} The submitter of the fixture(s), or the first author of the first fixture.
   */
  function getSubmitterNameMarkdown(user) {
    if (user) {
      return `@${user.login}`;
    }

    if (githubUsername) {
      return `**${githubUsername}**`;
    }

    const firstFixtureKey = Object.keys(fixtures)[0];
    const firstFixtureAuthor = fixtures[firstFixtureKey].meta.authors[0];

    return `**${firstFixtureAuthor}**`;
  }

  /**
   * @callback newContentFunction
   * @param {String|null} oldFileContent The text content of the old file, or null if it has not existed yet.
   * @returns {String} The new file text content.
   */

  /**
   * @param {String} filename The path of the file (relative to the repository root) to add or update.
   * @param {String} displayName A shorter file name to display in commit messages and PR body.
   * @param {newContentFunction} newContentFunction The callback to get the new file content from.
   * @param {Object|null} user GitHub user information about the submitter, or null if they are not a GitHub user.
   * @param {Boolean} [excludeFromCi=false] True if this commit should be excluded from the CI (continuous integration tests).
   * @returns {Promise} Promise that is resolved when the file is successfully / erroneously updated / added.
   */
  async function addOrUpdateFile(filename, displayName, newContentFunction, user, excludeFromCi = false) {
    const appendToCommitMessage = excludeFromCi ? `\n\n[skip ci]` : ``;

    let action;
    let sha;
    let newFileContent;

    console.log(`does ${displayName} exist?`);

    try {
      const result = await githubClient.repos.getContent({
        owner: `OpenLightingProject`,
        repo: repository,
        path: filename,
      });

      console.log(`yes -> update it ...`);

      const oldFileContent = Buffer.from(result.data.content, result.data.encoding).toString(`utf8`);
      newFileContent = newContentFunction(oldFileContent);

      if (oldFileContent === newFileContent) {
        console.log(`no need to update, files are the same`);
        return;
      }

      action = `Update`;
      sha = result.data.sha;
    }
    catch (error) {
      console.log(`no -> create it ...`);
      action = `Add`;
      newFileContent = newContentFunction(null);
    }

    const committer = user && user.email ? {
      name: user.name,
      email: user.email,
    } : undefined;

    try {
      await githubClient.repos.createOrUpdateFileContents({
        owner: `OpenLightingProject`,
        repo: repository,
        path: filename,
        message: `${action} ${displayName} via editor${appendToCommitMessage}`,
        content: encodeBase64(newFileContent),
        sha,
        branch: branchName,
        committer,
      });

      console.log(`done`);
      changedFiles.push(`${action} ${displayName}`);
    }
    catch (error) {
      console.error(`Error (${action.toLowerCase()} ${displayName}): ${error.message}`);
      githubErrors.push(`Error (${action.toLowerCase()} ${displayName}): \`${error.message}\``);
    }
  }
};

/**
 * @param {String} string The string to encode in base 64.
 * @returns {String} The encoded string.
 */
function encodeBase64(string) {
  return Buffer.from(string).toString(`base64`);
}

/**
 * JSON.stringify with an extra newline character at the end.
 * @param {Object} obj JSON object.
 * @returns {String} String representing the given object.
 */
function prettyJsonStringify(obj) {
  const str = JSON.stringify(obj, null, 2);

  return `${str}\n`;
}

<commit_msg>Mention all fixture authors when creating pull request (#1347)<commit_after>const fixtureJsonStringify = require(`./fixture-json-stringify.js`);
const { getObjectSortedByKeys } = require(`./register.js`);

require(`./load-env-file.js`);

const { Octokit } = require(`@octokit/rest`);

/** @typedef {import('./types.js').FixtureCreateResult} FixtureCreateResult */

/**
 * @param {FixtureCreateResult} fixtureCreateResult The object like returned by an import plugin.
 * @param {String|null} [githubUsername=null] GitHub user name of the fixture submitter.
 * @param {String|null} [githubComment=null] Additional comment for the pull request.
 * @returns {Promise.<String, Error>} A promise that resolves to the pull request URL, or rejects with an error.
 */
module.exports = async function createPullRequest(fixtureCreateResult, githubUsername = null, githubComment = null) {
  const {
    manufacturers,
    fixtures,
    warnings,
    errors,
  } = fixtureCreateResult;

  const repository = process.env.NODE_ENV === `production` ? `open-fixture-library` : `ofl-test`;

  const userToken = process.env.GITHUB_USER_TOKEN;
  if (userToken === undefined) {
    console.error(`.env file does not contain GITHUB_USER_TOKEN variable`);
    throw new Error(`GitHub user token was not set`);
  }

  const timestamp = new Date().toISOString().replace(/:/g, `-`).replace(/\..+/, ``);
  const branchName = `branch${timestamp}`;

  const changedFiles = [];
  const githubErrors = [];

  const githubClient = new Octokit({
    auth: `token ${userToken}`,
  });

  try {
    console.log(`get latest commit hash ...`);
    const latestCommitHash = (await githubClient.git.getRef({
      owner: `OpenLightingProject`,
      repo: repository,
      ref: `heads/master`,
    })).data.object.sha;
    console.log(latestCommitHash);

    console.log(`create new branch '${branchName}' ...`);
    await githubClient.git.createRef({
      owner: `OpenLightingProject`,
      repo: repository,
      ref: `refs/heads/${branchName}`,
      sha: latestCommitHash,
    });
    console.log(`done`);

    const user = await getGithubUserInfo();

    const skipCi = Object.keys(fixtures).length > 0;
    await addOrUpdateFile(`fixtures/manufacturers.json`, `manufacturers.json`, oldFileContent => {
      const mergedManufacturers = Object.assign(
        oldFileContent ? JSON.parse(oldFileContent) : {
          $schema: `https://raw.githubusercontent.com/OpenLightingProject/open-fixture-library/master/schemas/manufacturers.json`,
        },
        manufacturers,
      );

      return prettyJsonStringify(getObjectSortedByKeys(mergedManufacturers));
    }, user, skipCi);

    const fixtureEntries = Object.entries(fixtures);
    let index = 0;
    for (const [fixtureKey, fixture] of fixtureEntries) {
      await addOrUpdateFile(
        `fixtures/${fixtureKey}.json`,
        `fixture '${fixtureKey}'`,
        oldFileContent => fixtureJsonStringify(fixture),
        user,
        index !== fixtureEntries.length - 1, // all but the last fixture should skip CI
      );
      index++;
    }

    console.log(`create pull request ...`);

    const addedFixtures = changedFiles.filter(line => line.startsWith(`Add fixture`));
    let title = `Add ${addedFixtures.length} new fixtures`;
    if (addedFixtures.length === 1) {
      title = addedFixtures[0];
    }

    const result = await githubClient.pulls.create({
      owner: `OpenLightingProject`,
      repo: repository,
      title: title,
      body: getPrDescriptionMarkdown(user),
      head: branchName,
      base: `master`,
    });
    console.log(`done`);

    console.log(`add labels to pull request ...`);
    await githubClient.issues.addLabels({
      owner: `OpenLightingProject`,
      repo: repository,
      'issue_number': result.data.number,
      labels: [`new-fixture`, `via-editor`],
    });
    console.log(`done`);

    const pullRequestUrl = result.data.html_url;
    console.log(`View the pull request at ${pullRequestUrl}`);

    return pullRequestUrl;
  }
  catch (error) {
    console.error(`Error: ${error.message}`);
    throw error;
  }

  /**
   * @returns {Object|null} GitHub user information about the submitter, or null if it is not a valid GitHub user.
   */
  async function getGithubUserInfo() {
    if (!githubUsername) {
      return null;
    }

    console.log(`get info for GitHub user @${githubUsername} ...`);

    try {
      const user = (await githubClient.users.getByUsername({
        username: githubUsername,
      })).data;

      console.log(`done (name: ${user.name}, email: ${user.email})`);
      return user;
    }
    catch (error) {
      console.log(`error getting user`);
      return null;
    }
  }

  /**
   * @param {Object|null} user GitHub user information about the submitter, or null if they are not a GitHub user.
   * @returns {String} The pull request description body markdown.
   */
  function getPrDescriptionMarkdown(user) {
    let body = changedFiles.map(line => `* ${line}`).join(`\n`);

    if (githubErrors.length > 0) {
      body += `\n\n### Errors:\n* :warning: ${githubErrors.join(`\n* :warning: `)}`;
    }

    const fixtureWarningsErrors = getFixtureWarningsErrorsMarkdownList();
    if (fixtureWarningsErrors.length) {
      body += `\n\n### Fixture warnings / errors\n\n`;
      body += fixtureWarningsErrors;
    }

    if (githubComment) {
      body += `\n\n### User comment\n\n`;
      body += githubComment;
    }

    body += `\n\nThank you ${getSubmitterNameMarkdown(user)}!`;

    return body;
  }

  /**
   * @returns {String} A Markdown list of all fixture warnings and errors.
   */
  function getFixtureWarningsErrorsMarkdownList() {
    let markdownList = ``;

    for (const fixtureKey of Object.keys(fixtures)) {
      const fixtureErrors = errors[fixtureKey] || [];
      const fixtureWarnings = warnings[fixtureKey] || [];

      const messages = fixtureErrors.map(
        error => `  - :x: ${error}`,
      ).concat(fixtureWarnings.map(
        warning => `  - :warning: ${warning}`,
      ));

      if (messages.length) {
        markdownList += `* ${fixtureKey}\n${messages.join(`\n`)}\n`;
      }
    }

    return markdownList;
  }

  /**
   * @param {Object|null} user GitHub user information about the submitter, or null if they are not a GitHub user.
   * @returns {String} The submitter of the fixture(s), or the first author of the first fixture.
   */
  function getSubmitterNameMarkdown(user) {
    if (user) {
      return `@${user.login}`;
    }

    if (githubUsername) {
      return `**${githubUsername}**`;
    }

    return Object.values(fixtures)
      .flatMap(fixture => fixture.meta.authors)
      .filter((author, index, list) => list.indexOf(author) === index) // filter out duplicates
      .map(author => `**${author}**`)
      .join(` and `);
  }

  /**
   * @callback newContentFunction
   * @param {String|null} oldFileContent The text content of the old file, or null if it has not existed yet.
   * @returns {String} The new file text content.
   */

  /**
   * @param {String} filename The path of the file (relative to the repository root) to add or update.
   * @param {String} displayName A shorter file name to display in commit messages and PR body.
   * @param {newContentFunction} newContentFunction The callback to get the new file content from.
   * @param {Object|null} user GitHub user information about the submitter, or null if they are not a GitHub user.
   * @param {Boolean} [excludeFromCi=false] True if this commit should be excluded from the CI (continuous integration tests).
   * @returns {Promise} Promise that is resolved when the file is successfully / erroneously updated / added.
   */
  async function addOrUpdateFile(filename, displayName, newContentFunction, user, excludeFromCi = false) {
    const appendToCommitMessage = excludeFromCi ? `\n\n[skip ci]` : ``;

    let action;
    let sha;
    let newFileContent;

    console.log(`does ${displayName} exist?`);

    try {
      const result = await githubClient.repos.getContent({
        owner: `OpenLightingProject`,
        repo: repository,
        path: filename,
      });

      console.log(`yes -> update it ...`);

      const oldFileContent = Buffer.from(result.data.content, result.data.encoding).toString(`utf8`);
      newFileContent = newContentFunction(oldFileContent);

      if (oldFileContent === newFileContent) {
        console.log(`no need to update, files are the same`);
        return;
      }

      action = `Update`;
      sha = result.data.sha;
    }
    catch (error) {
      console.log(`no -> create it ...`);
      action = `Add`;
      newFileContent = newContentFunction(null);
    }

    const committer = user && user.email ? {
      name: user.name,
      email: user.email,
    } : undefined;

    try {
      await githubClient.repos.createOrUpdateFileContents({
        owner: `OpenLightingProject`,
        repo: repository,
        path: filename,
        message: `${action} ${displayName} via editor${appendToCommitMessage}`,
        content: encodeBase64(newFileContent),
        sha,
        branch: branchName,
        committer,
      });

      console.log(`done`);
      changedFiles.push(`${action} ${displayName}`);
    }
    catch (error) {
      console.error(`Error (${action.toLowerCase()} ${displayName}): ${error.message}`);
      githubErrors.push(`Error (${action.toLowerCase()} ${displayName}): \`${error.message}\``);
    }
  }
};

/**
 * @param {String} string The string to encode in base 64.
 * @returns {String} The encoded string.
 */
function encodeBase64(string) {
  return Buffer.from(string).toString(`base64`);
}

/**
 * JSON.stringify with an extra newline character at the end.
 * @param {Object} obj JSON object.
 * @returns {String} String representing the given object.
 */
function prettyJsonStringify(obj) {
  const str = JSON.stringify(obj, null, 2);

  return `${str}\n`;
}

<|endoftext|>


<commit_before>module.exports = function () {

  var path           = require('path');
  var Promise        = require('bluebird');
  var SessionManager = require('../session-manager.js');
  var chaiAsPromised = require('chai-as-promised');
  var colors         = require('colors');
  var log            = require('../log');
  var fs             = require('fs-extra');

  this.registerHandler('BeforeFeatures', function (event) {

    log.debug('[chimp][hooks] Starting BeforeFeatures');

    var setupBrowser = function () {
      log.debug('[chimp][world] getting browser');
      var customChimpConfigPath = path.resolve(process.cwd(), process.env['chimp.path'], 'chimp.js');

      var _translateLogLevel = function () {
        if (process.env['chimp.log'] === 'info' ||
          process.env['chimp.log'] === 'warn' ||
          process.env['chimp.log'] === 'error') {
          return 'silent'
        }
        return process.env['chimp.log'];
      };

      global.sessionManager = new SessionManager({
        port: process.env['chimp.port'],
        browser: process.env['chimp.browser'],
        deviceName: process.env['chimp.deviceName']
      });

      if (fs.existsSync(customChimpConfigPath)) {
        var customChimpConfigurator = wrapAsync(require(customChimpConfigPath));
        global.browser = customChimpConfigurator(global.sessionManager);
      } else {
        log.debug('[chimp][world] custom chimp.js not found, loading defaults');
        var webdriverOptions = {
          waitforTimeout: parseInt(process.env['chimp.waitForTimeout']),
          desiredCapabilities: {
            browserName: process.env['chimp.browser'],
            platform: process.env['chimp.platform'],
            name: process.env['chimp.name']
          },
          user: process.env['chimp.user'] || process.env.SAUCE_USERNAME,
          key: process.env['chimp.key'] || process.env.SAUCE_ACCESS_KEY,
          host: process.env['chimp.host'],
          port: process.env['chimp.port'],
          logLevel: _translateLogLevel(),
          screenshotPath: process.env['chimp.screenshotPath'],
          sync: process.env['chimp.sync'] !== 'false'
        };
        log.debug('[chimp][world] webdriverOptions are ', JSON.stringify(webdriverOptions));

        var remoteSession = wrapAsync(global.sessionManager.remote, global.sessionManager);
        global.browser = remoteSession(webdriverOptions);
      }

      chaiAsPromised.transferPromiseness = global.browser.transferPromiseness;
    };

    var initBrowser = function () {
      log.debug('[chimp][hooks] init browser');
      var browser = global.browser;
      browser.initSync();
      log.debug('[chimp][hooks] init browser callback');

      browser.screenshotsCount = 0;
      browser.addCommand('capture', function (name) {
        var location = (browser.screenshotsCount++) + '-' + name + '_' + new Date().getTime() + '.png';
        fs.mkdirsSync(process.env['chimp.screenshotsPath']);
        var ssPath = path.join(process.env['chimp.screenshotsPath'], location);
        log.debug('[chimp][hooks] saving screenshot to', ssPath);
        this.saveScreenshot(ssPath, false);
        log.debug('[chimp][hooks] saved screenshot to', ssPath);
      });

      browser.timeoutsAsyncScriptSync(parseInt(process.env['chimp.timeoutsAsyncScript']));
      log.debug('[chimp][hooks] set timeoutsAsyncScript');

      if (process.env['chimp.browser'] === 'phantomjs') {
        browser.setViewportSizeSync({
          width: process.env['chimp.phantom_w'] || 1280,
          height: process.env['chimp.phantom_h'] || 1024
        });
      }
    };

    var setupDdp = function () {
      log.debug('[chimp][hooks] setup DDP');
      if (process.env['chimp.ddp']) {
        log.debug('[chimp][hooks] connecting via DDP to', process.env['chimp.ddp']);
        try {
          global.ddp.connectSync();
          log.info('[chimp][hooks] connecting via DDP had no error');
        } catch (error) {
          log.error('[chimp][hooks] connecting via DDP error', error);
        }
      } else {
        log.debug('[chimp][hooks] DDP not required');
      }
    };

    try {
      setupBrowser();
      initBrowser();
      setupDdp();
    } catch (error) {
      log.error('[chimp][hooks] BeforeFeatures hook had error');
      log.error(error);
      process.exit(2);
    }

    log.debug('[chimp][hooks] Finished BeforeFeatures');
    if (this.UserDefinedBeforeFeatures) {
      log.debug('[chimp][hooks] User-defined BeforeFeatures found, calling');
      // TODO: Wrap in the same thing as the Cucumber words (see cucumber-wrapper.js)
      global.UserDefinedBeforeFeatures.apply(this);
    } else {
      log.debug('[chimp][hooks] User-defined BeforeFeatures not found, finishing up');
    }
  });

  /**
   * Store browser test status so we can notify SauceLabs
   *
   * @param {Function} event
   */
  var lastStep;
  this.StepResult(function (event) {
    var stepResult = event.getPayloadItem('stepResult');
    lastStep = stepResult.getStep();
    if (process.env['chimp.screenshotsOnError'] !== 'false' && !stepResult.isSuccessful()) {
      log.debug('[chimp][hooks] step failed - capturing screenshot');
      global.browser.captureSync('step_failed');
    }
  });

  /**
   * After features have run we close the browser and optionally notify
   * SauceLabs
   *
   * @param {Function} event
   * @param {Function} callback
   */
  this.registerHandler('AfterFeatures', function (event) {
    log.debug('[chimp][hooks] Starting AfterFeatures');

    if (process.env['chimp.browser'] !== 'phantomjs') {
      log.debug('[chimp][hooks] Ending browser session');

      wrapAsync(global.sessionManager.killCurrentSession, global.sessionManager)();
      log.debug('[chimp][hooks] Ended browser sessions');
    }

    log.debug('[chimp][hooks] Finished AfterFeatures');
  });

  process.on('unhandledRejection', function (reason, promise) {
    log.error('[chimp] Detected an unhandledRejection.'.red);

    try {
      if (reason.type === 'CommandError' && reason.message === 'Promise never resolved with an truthy value') {
        reason.type += 'WebdriverIO CommandError (Promise never resolved with an truthy value)';
        reason.message = 'This usually happens when WebdriverIO assertions fail or timeout.';
        var hint = 'HINT: Check the step AFTER [' + lastStep.getKeyword() + lastStep.getName() + ']';
        var uri = lastStep.getUri();
        uri = uri.substring(process.cwd().length, uri.length);
        hint += ' (' + uri + ': >' + lastStep.getLine() + ')';
        log.error('[chimp][hooks] Reason:'.red);
        log.error('[chimp][hooks]'.red, reason.type.red);
        log.error('[chimp][hooks]'.red, reason.message.red);
        log.error(hint.yellow);
        reason.message += '\n' + hint;
      } else {
        log.error('[chimp][hooks] Reason:'.red);
        log.error('[chimp][hooks]'.red, reason.type.red);
        log.error('[chimp][hooks]'.red, reason.message.red);
      }
    } catch (e) {
      log.debug('[chimp][hooks] Could not provide error hint');
    }

    log.debug('[chimp][hooks] Promise:', JSON.parse(JSON.stringify(promise)));
    log.debug('[chimp][hooks] Forcibly exiting Cucumber');

    process.send(JSON.stringify(reason));
    process.exit(2);
  });

  process.on('SIGINT', function () {
    log.debug('[chimp][hooks] Received SIGINT process event, ending browser session');
    global.browser.
      endAsync().
      then(function () {
        log.debug('[chimp][hooks] ended browser session');
        process.exit();
      });
  });

};
<commit_msg>Log stack trace when BeforeFeatures fails<commit_after>module.exports = function () {

  var path           = require('path');
  var Promise        = require('bluebird');
  var SessionManager = require('../session-manager.js');
  var chaiAsPromised = require('chai-as-promised');
  var colors         = require('colors');
  var log            = require('../log');
  var fs             = require('fs-extra');

  this.registerHandler('BeforeFeatures', function (event) {

    log.debug('[chimp][hooks] Starting BeforeFeatures');

    var setupBrowser = function () {
      log.debug('[chimp][world] getting browser');
      var customChimpConfigPath = path.resolve(process.cwd(), process.env['chimp.path'], 'chimp.js');

      var _translateLogLevel = function () {
        if (process.env['chimp.log'] === 'info' ||
          process.env['chimp.log'] === 'warn' ||
          process.env['chimp.log'] === 'error') {
          return 'silent'
        }
        return process.env['chimp.log'];
      };

      global.sessionManager = new SessionManager({
        port: process.env['chimp.port'],
        browser: process.env['chimp.browser'],
        deviceName: process.env['chimp.deviceName']
      });

      if (fs.existsSync(customChimpConfigPath)) {
        var customChimpConfigurator = wrapAsync(require(customChimpConfigPath));
        global.browser = customChimpConfigurator(global.sessionManager);
      } else {
        log.debug('[chimp][world] custom chimp.js not found, loading defaults');
        var webdriverOptions = {
          waitforTimeout: parseInt(process.env['chimp.waitForTimeout']),
          desiredCapabilities: {
            browserName: process.env['chimp.browser'],
            platform: process.env['chimp.platform'],
            name: process.env['chimp.name']
          },
          user: process.env['chimp.user'] || process.env.SAUCE_USERNAME,
          key: process.env['chimp.key'] || process.env.SAUCE_ACCESS_KEY,
          host: process.env['chimp.host'],
          port: process.env['chimp.port'],
          logLevel: _translateLogLevel(),
          screenshotPath: process.env['chimp.screenshotPath'],
          sync: process.env['chimp.sync'] !== 'false'
        };
        log.debug('[chimp][world] webdriverOptions are ', JSON.stringify(webdriverOptions));

        var remoteSession = wrapAsync(global.sessionManager.remote, global.sessionManager);
        global.browser = remoteSession(webdriverOptions);
      }

      chaiAsPromised.transferPromiseness = global.browser.transferPromiseness;
    };

    var initBrowser = function () {
      log.debug('[chimp][hooks] init browser');
      var browser = global.browser;
      browser.initSync();
      log.debug('[chimp][hooks] init browser callback');

      browser.screenshotsCount = 0;
      browser.addCommand('capture', function (name) {
        var location = (browser.screenshotsCount++) + '-' + name + '_' + new Date().getTime() + '.png';
        fs.mkdirsSync(process.env['chimp.screenshotsPath']);
        var ssPath = path.join(process.env['chimp.screenshotsPath'], location);
        log.debug('[chimp][hooks] saving screenshot to', ssPath);
        this.saveScreenshot(ssPath, false);
        log.debug('[chimp][hooks] saved screenshot to', ssPath);
      });

      browser.timeoutsAsyncScriptSync(parseInt(process.env['chimp.timeoutsAsyncScript']));
      log.debug('[chimp][hooks] set timeoutsAsyncScript');

      if (process.env['chimp.browser'] === 'phantomjs') {
        browser.setViewportSizeSync({
          width: process.env['chimp.phantom_w'] || 1280,
          height: process.env['chimp.phantom_h'] || 1024
        });
      }
    };

    var setupDdp = function () {
      log.debug('[chimp][hooks] setup DDP');
      if (process.env['chimp.ddp']) {
        log.debug('[chimp][hooks] connecting via DDP to', process.env['chimp.ddp']);
        try {
          global.ddp.connectSync();
          log.info('[chimp][hooks] connecting via DDP had no error');
        } catch (error) {
          log.error('[chimp][hooks] connecting via DDP error', error);
        }
      } else {
        log.debug('[chimp][hooks] DDP not required');
      }
    };

    try {
      setupBrowser();
      initBrowser();
      setupDdp();
    } catch (error) {
      log.error('[chimp][hooks] BeforeFeatures hook had error');
      log.error(error);
      log.error(error.stack);
      process.exit(2);
    }

    log.debug('[chimp][hooks] Finished BeforeFeatures');
    if (this.UserDefinedBeforeFeatures) {
      log.debug('[chimp][hooks] User-defined BeforeFeatures found, calling');
      // TODO: Wrap in the same thing as the Cucumber words (see cucumber-wrapper.js)
      global.UserDefinedBeforeFeatures.apply(this);
    } else {
      log.debug('[chimp][hooks] User-defined BeforeFeatures not found, finishing up');
    }
  });

  /**
   * Store browser test status so we can notify SauceLabs
   *
   * @param {Function} event
   */
  var lastStep;
  this.StepResult(function (event) {
    var stepResult = event.getPayloadItem('stepResult');
    lastStep = stepResult.getStep();
    if (process.env['chimp.screenshotsOnError'] !== 'false' && !stepResult.isSuccessful()) {
      log.debug('[chimp][hooks] step failed - capturing screenshot');
      global.browser.captureSync('step_failed');
    }
  });

  /**
   * After features have run we close the browser and optionally notify
   * SauceLabs
   *
   * @param {Function} event
   * @param {Function} callback
   */
  this.registerHandler('AfterFeatures', function (event) {
    log.debug('[chimp][hooks] Starting AfterFeatures');

    if (process.env['chimp.browser'] !== 'phantomjs') {
      log.debug('[chimp][hooks] Ending browser session');

      wrapAsync(global.sessionManager.killCurrentSession, global.sessionManager)();
      log.debug('[chimp][hooks] Ended browser sessions');
    }

    log.debug('[chimp][hooks] Finished AfterFeatures');
  });

  process.on('unhandledRejection', function (reason, promise) {
    log.error('[chimp] Detected an unhandledRejection.'.red);

    try {
      if (reason.type === 'CommandError' && reason.message === 'Promise never resolved with an truthy value') {
        reason.type += 'WebdriverIO CommandError (Promise never resolved with an truthy value)';
        reason.message = 'This usually happens when WebdriverIO assertions fail or timeout.';
        var hint = 'HINT: Check the step AFTER [' + lastStep.getKeyword() + lastStep.getName() + ']';
        var uri = lastStep.getUri();
        uri = uri.substring(process.cwd().length, uri.length);
        hint += ' (' + uri + ': >' + lastStep.getLine() + ')';
        log.error('[chimp][hooks] Reason:'.red);
        log.error('[chimp][hooks]'.red, reason.type.red);
        log.error('[chimp][hooks]'.red, reason.message.red);
        log.error(hint.yellow);
        reason.message += '\n' + hint;
      } else {
        log.error('[chimp][hooks] Reason:'.red);
        log.error('[chimp][hooks]'.red, reason.type.red);
        log.error('[chimp][hooks]'.red, reason.message.red);
      }
    } catch (e) {
      log.debug('[chimp][hooks] Could not provide error hint');
    }

    log.debug('[chimp][hooks] Promise:', JSON.parse(JSON.stringify(promise)));
    log.debug('[chimp][hooks] Forcibly exiting Cucumber');

    process.send(JSON.stringify(reason));
    process.exit(2);
  });

  process.on('SIGINT', function () {
    log.debug('[chimp][hooks] Received SIGINT process event, ending browser session');
    global.browser.
      endAsync().
      then(function () {
        log.debug('[chimp][hooks] ended browser session');
        process.exit();
      });
  });

};
<|endoftext|>


<commit_before>Stocks = new Mongo.Collection("stocks");

// const listexample = {
// 	name: "ex",
// 	virgin: 1,
// 	user_id: "_000",
// };
// stockSchema.validate(listexample);

// server oriented code
if(Meteor.isServer) {

	stockSchema = new SimpleSchema({
		name: {type: String},
		virgin: {type: Number, defaultValue: 1, allowedValues: [0,1]},
		seed: {type: String, optional: true},
		fertility: {type: String, optional: true, defaultValue: "unknown", allowedValues: ["unknown", "none", "low", "medium", "high"]},
		status: {type: String, defaultValue: "queue", allowedValues: ["queue", "target", "abort"]},
		vndb_id: {type: Number, optional: true},
		user_id: {type: String, regEx: SimpleSchema.RegEx.Id},
		created_at: {type: Date, defaultValue: new Date()},
		updated_at: {type: Date, defaultValue: new Date()},
	});

	stockUpdateSchema = new SimpleSchema({
		name: {type: String, optional: true},
		virgin: {type: Number, optional: true, allowedValues: [0,1]},
		seed: {type: String, optional: true},
		fertility: {type: String, optional: true, allowedValues: ["unknown", "none", "low", "medium", "high"]},
		status: {type: String, optional: true, allowedValues: ["queue", "target", "abort"]},
		vndb_id: {type: Number, optional: true},
		user_id: {type: String, optional: true, regEx: SimpleSchema.RegEx.Id},
		created_at: {type: Date, optional: true},
		updated_at: {type: Date, defaultValue: new Date()},
	})

	// const MAX_STOCKS = 9999;

	// publisher for stocks
	Meteor.publish('stocks', function(limit) {
		if(!this.userId) {
			// explanation says returning ready state with empty content let client know it's not loading forever
			return this.ready();
		}

		limit = limit || 0;
		// validating limit request value
		new SimpleSchema({
			limit: {type: Number, optional: true},
		}).validate({ limit });

		// const options = {
		// 	sort: {created_at: -1},
		// 	limit: Math.min(limit, MAX_STOCKS),
		// }

		return Stocks.find({
			$or: [
				{ virgin: {$ne: 1} },
				{ user_id: this.userId },
			]
		}, {
			limit: limit,
		});
	});

	Meteor.methods({
		getTotalSlavesCunt: function() {
			return Stocks.find().count();
		},
		getDirtySlavesCunt: function() {
			return Stocks.find({
				virgin: {$eq: 0},
			}).count();
		},
		getCleanSlavesCunt: function() {
			return Stocks.find({
				virgin: {$eq: 1},
			}).count();
		},
		updateVirginity: function(stock_id, data) {
			Future = Npm.require('fibers/future');
			var mirai = new Future;

			var stock = Stocks.findOne(stock_id);
			if(stock.user_id !== Meteor.userId()) {
				throw new Meteor.Error("not-authorized");
			}

			// translate virginity to integer
			data.virgin = Number(data.virgin);
			if(!data.virgin && data.virgin != 0) {
				throw new Meteor.Error("invalid-type", "Invalid type of Number used against virginity.");
			}

			// validte against schema
			stockUpdateSchema.clean(data);
			stockUpdateSchema.validate(data);

			Stocks.update(stock_id, {
				$set: {
					virgin: data.virgin,
				}
			}, function(error, result) {
				if(error) {
					mirai.throw(error);
					console.log("ERROR", error);
				}
				if(result) {
					mirai.return(result);
				}
			});

			return mirai.wait();
		},
		updateFertility: function(stock_id, data) {
			// load future
			Future = Npm.require('fibers/future');
			var myFuture = new Future;

			var stock = Stocks.findOne(stock_id);
			if(stock.user_id !== Meteor.userId()) {
				throw new Meteor.Error("not-authorized");
			}

			stockUpdateSchema.clean(data);
			stockUpdateSchema.validate(data);

			Stocks.update(stock_id, {
				$set: {
					fertility: data.fertility,
				}
			}, function(error, result) {
				if(error) {
					myFuture.throw(error);
					console.log("ERROR", error);
				}
				if(result) {
					myFuture.return(result);
					// return {status: "success"};
				}
			});

			return myFuture.wait();
		},
		updateStatus: function(stock_id, data) {
			Future = Npm.require('fibers/future');
			var mirai = new Future;

			var stock = Stocks.findOne(stock_id);
			if(stock.user_id !== Meteor.userId()) {
				throw new Meteor.Error("not-authorized");
			}

			stockUpdateSchema.clean(data);
			stockUpdateSchema.validate(data);

			Stocks.update(stock_id, {
				$set: {
					status: data.status,
				}
			}, function(error, result) {
				if(error) {
					mirai.throw(error);
					console.log("ERROR", error);
				}
				if(result) {
					mirai.return(result);
				}
			});

			return mirai.wait();
		},
	})

	Meteor.startup(function () {
		// code to run on server at startup
	});
}

// term fecundation

// client oriented code
if (Meteor.isClient) {

	Accounts.ui.config({
		passwordSignupFields: "USERNAME_ONLY",
	});

	// Meteor.subscribe('stocks');

	var slaves_expansion = 20;
	Session.setDefault('slaves_limit', slaves_expansion);
	Tracker.autorun(function() {
		Meteor.subscribe('stocks', Session.get('slaves_limit'));
	});

	Template.body.helpers({
		// helper for body
	});

	Template.anal.helpers({
		slaves: function() {
			return Stocks.find();
		},
		moreResults: function() {
			return !(Stocks.find().count() < Session.get("slaves_limit"));
		},
		virginity_check: function() {
			if(this.virgin == 1) {
				return "checked";
			}
			else {
				return "";
			}
			return this;
		}
	});

	// setInterval(function() {

	// }, 1000 * 1);
	Template.climax.helpers({
		current: function() {
			return Stocks.find().count();
		},
		total: function() {
			Session.setDefault("total_slaves_cunt", 0);
			Meteor.call('getTotalSlavesCunt', function(error, response) {
				// console.log("ERROR", error, "RESPONSE", response);
				Session.set("total_slaves_cunt", response);
			});
			return Session.get("total_slaves_cunt");
		},
		dirty: function() {
			Session.setDefault("dirty_slaves_cunt", 0);
			Meteor.call('getDirtySlavesCunt', function(error, response) {
				// console.log("ERROR", error, "RESPONSE", response);
				Session.set("dirty_slaves_cunt", response);
			});
			return Session.get("dirty_slaves_cunt");
		},
		clean: function() {
			Session.setDefault("clean_slaves_cunt", 0);
			Meteor.call('getCleanSlavesCunt', function(error, response) {
				// console.log("ERROR", error, "RESPONSE", response);
				Session.set("clean_slaves_cunt", response);
			});
			return Session.get("clean_slaves_cunt");
		},
	});

	// whenever #showMoreResults becomes visible, retieve more results
	function loadMore() {
		var threshold, target = $("body");
		if(!target.length) return;

		threshold = $(window).scrollTop() + $(window).height() - target.height();

		// target.height() calculate the height of overall document body
		// window height() calculate the height of browser client area (document), substracted to remove the gap from top edge scrollbar to bottom of document body height
		var distance = target.height() - $(window).height();

		// console.log("DIST:"+distance+
		// 	" BODY:"+  target.height() +
		// 	" SCROLLTOP:"+$(window).scrollTop() +
		// 	" WINDOW:"+ $(window).height()+
		// 	" OFFSET:"+ target.offset().top+
		// 	" DOC:" + $(document).height()+
		// 	" POINT:" + ($(document).height() - $(window).height())
		// 	);

		// if(target.offset().top < threshold) {
		// 	console.log("TRIGGER LOAD");
		// 	Session.set("slaves_limit", Session.get("slaves_limit") + slaves_expansion);
		// }
		// else {
		// 	if(target.data("visible")) {
		// 		target.data("visible", false);
		// 	}
		// }

		if($(window).scrollTop() == $(document).height() - $(window).height()) {
			Session.set("slaves_limit", Session.get("slaves_limit") + slaves_expansion);
		}
	}

	Template.injection.events({
		"submit .new-slave": function(event) {
			event.preventDefault();
			var val = event.target.text.value;
			Meteor.call("addSlave", val);
			event.target.text.value = "";
		},
	});

	$(window).on('beforeunload', function() {
		$(window).scrollTop(0);
	});

	Meteor.startup(function () {
		// code to run on client at startup
		$(window).scroll(loadMore);
	});

	// Template.anal.onRendered(function() {
	// 	this.$(".fertility-dropdown").dropdown();
	// });

	Template.anal.events({
		"mousedown .stock-item": function(event) {
			var stock_id = $(event.target).find("input[name=stockid]").val();
			// var target_element = $(event.target).find("input[name=stockid][value=" + stock_id + "]");
			// console.log(target_element);
			// $("
xxxxxxxxxxxxxxxxx
").appendTo(event.target);
			var target_element = $(event.target).find(".stock-control");
			target_element.transition({
				animation: 'drop',
				duration: '100ms',
				queue: true,
			});
			if(this.virgin == 1) {
				$(event.target).find(".virginity-checkbox").checkbox("set checked");
			}
			else {
				$(event.target).find(".virginity-checkbox").checkbox();
			}
			$(event.target).find(".fertility-dropdown").dropdown();
			$(event.target).find(".status-dropdown").dropdown();
		},
		"mouseenter .virginity-checkbox": function(event) {
		},
		"change .virginity-checkbox": function(event) {
			var virginity = $(event.target).prop('checked') ? '1' : '0';
			// prevent update request happens despite same value, this occurs due to on position switch being applied when clicking-to-expand item
			if(virginity == this.virgin) {
				return;
			}
			data = {
				virgin: virginity,
			};
			Meteor.call("updateVirginity", this._id, data, function(error, response) {
				if(error) {
					console.log(error);
				}
				else {
					showNotification("Virginity lost", "none");
				}
			});
		},
		// "mouseenter .fertility-dropdown": function(event) {
		// 	$(event.target).dropdown();
		// },
		"change .fertility-input": function(event) {
			data = {
				fertility: event.target.value,
			};
			Meteor.call("updateFertility", this._id, data, function(error, response) {
				if(error) {
					console.log(error);
				}
				else {
					showNotification("Womb fertilized", "none");
				}
			});
		},
		// "mouseenter .status-dropdown": function(event) {
		// 	$(event.target).dropdown();
		// },
		"change .status-input": function(event) {
			data = {
				status: event.target.value,
			}
			Meteor.call("updateStatus", this._id, data, function(error, response) {
				if(error) {
					console.log(error);
				}
				else {
					showNotification("Status updated", "none");
				}
			});
		},
	});

	function showNotification(header_message, content_message) {
		var message = {
			header: header_message,
			content: new Date(),
		};
		Session.set("notification", message);
		$(".notification")
			.transition({
				animation: 'scale',
				duration: '0.3s',
			})
			.transition({
				allowRepeats: true,
				animation: 'pulse',
				duration: '2s',
			})
			.transition({
				animation: 'scale',
				duration: '1.5s',
			})
		;
	}

	Template.notification.helpers({
		header: function() {
			Session.setDefault("notification", {header: "idle", content: "no content"});
			return Session.get("notification").header;
		},
		content: function() {
			Session.setDefault("notification", {header: "idle", content: "no content"});
			return Session.get("notification").content;
		},
	});

	// Tracker.autorun(function() {
	// 	var notification = Session.get("notification");
	// 	console.log("CHANGE", notification);
	// });

}
<commit_msg>Update simple-schema: include images property<commit_after>Stocks = new Mongo.Collection("stocks");

// const listexample = {
// 	name: "ex",
// 	virgin: 1,
// 	user_id: "_000",
// };
// stockSchema.validate(listexample);

// server oriented code
if(Meteor.isServer) {

	stockSchema = new SimpleSchema({
		name: {type: String},
		virgin: {type: Number, defaultValue: 1, allowedValues: [0,1]},
		seed: {type: String, optional: true},
		fertility: {type: String, optional: true, defaultValue: "unknown", allowedValues: ["unknown", "none", "low", "medium", "high"]},
		status: {type: String, defaultValue: "queue", allowedValues: ["queue", "target", "abort"]},
		vndb_id: {type: Number, optional: true},
		cover_image: {type: String, optional: true},
		sample_images: {type: [String], optional: true},
		user_id: {type: String, regEx: SimpleSchema.RegEx.Id},
		created_at: {type: Date, defaultValue: new Date()},
		updated_at: {type: Date, defaultValue: new Date()},
	});

	stockUpdateSchema = new SimpleSchema({
		name: {type: String, optional: true},
		virgin: {type: Number, optional: true, allowedValues: [0,1]},
		seed: {type: String, optional: true},
		fertility: {type: String, optional: true, allowedValues: ["unknown", "none", "low", "medium", "high"]},
		status: {type: String, optional: true, allowedValues: ["queue", "target", "abort"]},
		vndb_id: {type: Number, optional: true},
		cover_image: {type: String, optional: true},
		sample_images: {type: [String], optional: true},
		user_id: {type: String, optional: true, regEx: SimpleSchema.RegEx.Id},
		created_at: {type: Date, optional: true},
		updated_at: {type: Date, defaultValue: new Date()},
	})

	// const MAX_STOCKS = 9999;

	// publisher for stocks
	Meteor.publish('stocks', function(limit) {
		if(!this.userId) {
			// explanation says returning ready state with empty content let client know it's not loading forever
			return this.ready();
		}

		limit = limit || 0;
		// validating limit request value
		new SimpleSchema({
			limit: {type: Number, optional: true},
		}).validate({ limit });

		// const options = {
		// 	sort: {created_at: -1},
		// 	limit: Math.min(limit, MAX_STOCKS),
		// }

		return Stocks.find({
			$or: [
				{ virgin: {$ne: 1} },
				{ user_id: this.userId },
			]
		}, {
			limit: limit,
		});
	});

	Meteor.methods({
		getTotalSlavesCunt: function() {
			return Stocks.find().count();
		},
		getDirtySlavesCunt: function() {
			return Stocks.find({
				virgin: {$eq: 0},
			}).count();
		},
		getCleanSlavesCunt: function() {
			return Stocks.find({
				virgin: {$eq: 1},
			}).count();
		},
		updateVirginity: function(stock_id, data) {
			Future = Npm.require('fibers/future');
			var mirai = new Future;

			var stock = Stocks.findOne(stock_id);
			if(stock.user_id !== Meteor.userId()) {
				throw new Meteor.Error("not-authorized");
			}

			// translate virginity to integer
			data.virgin = Number(data.virgin);
			if(!data.virgin && data.virgin != 0) {
				throw new Meteor.Error("invalid-type", "Invalid type of Number used against virginity.");
			}

			// validte against schema
			stockUpdateSchema.clean(data);
			stockUpdateSchema.validate(data);

			Stocks.update(stock_id, {
				$set: {
					virgin: data.virgin,
				}
			}, function(error, result) {
				if(error) {
					mirai.throw(error);
					console.log("ERROR", error);
				}
				if(result) {
					mirai.return(result);
				}
			});

			return mirai.wait();
		},
		updateFertility: function(stock_id, data) {
			// load future
			Future = Npm.require('fibers/future');
			var myFuture = new Future;

			var stock = Stocks.findOne(stock_id);
			if(stock.user_id !== Meteor.userId()) {
				throw new Meteor.Error("not-authorized");
			}

			stockUpdateSchema.clean(data);
			stockUpdateSchema.validate(data);

			Stocks.update(stock_id, {
				$set: {
					fertility: data.fertility,
				}
			}, function(error, result) {
				if(error) {
					myFuture.throw(error);
					console.log("ERROR", error);
				}
				if(result) {
					myFuture.return(result);
					// return {status: "success"};
				}
			});

			return myFuture.wait();
		},
		updateStatus: function(stock_id, data) {
			Future = Npm.require('fibers/future');
			var mirai = new Future;

			var stock = Stocks.findOne(stock_id);
			if(stock.user_id !== Meteor.userId()) {
				throw new Meteor.Error("not-authorized");
			}

			stockUpdateSchema.clean(data);
			stockUpdateSchema.validate(data);

			Stocks.update(stock_id, {
				$set: {
					status: data.status,
				}
			}, function(error, result) {
				if(error) {
					mirai.throw(error);
					console.log("ERROR", error);
				}
				if(result) {
					mirai.return(result);
				}
			});

			return mirai.wait();
		},
	})

	Meteor.startup(function () {
		// code to run on server at startup
	});
}

// term fecundation

// client oriented code
if (Meteor.isClient) {

	Accounts.ui.config({
		passwordSignupFields: "USERNAME_ONLY",
	});

	// Meteor.subscribe('stocks');

	var slaves_expansion = 20;
	Session.setDefault('slaves_limit', slaves_expansion);
	Tracker.autorun(function() {
		Meteor.subscribe('stocks', Session.get('slaves_limit'));
	});

	Template.body.helpers({
		// helper for body
	});

	Template.anal.helpers({
		slaves: function() {
			return Stocks.find();
		},
		moreResults: function() {
			return !(Stocks.find().count() < Session.get("slaves_limit"));
		},
		virginity_check: function() {
			if(this.virgin == 1) {
				return "checked";
			}
			else {
				return "";
			}
			return this;
		}
	});

	// setInterval(function() {

	// }, 1000 * 1);
	Template.climax.helpers({
		current: function() {
			return Stocks.find().count();
		},
		total: function() {
			Session.setDefault("total_slaves_cunt", 0);
			Meteor.call('getTotalSlavesCunt', function(error, response) {
				// console.log("ERROR", error, "RESPONSE", response);
				Session.set("total_slaves_cunt", response);
			});
			return Session.get("total_slaves_cunt");
		},
		dirty: function() {
			Session.setDefault("dirty_slaves_cunt", 0);
			Meteor.call('getDirtySlavesCunt', function(error, response) {
				// console.log("ERROR", error, "RESPONSE", response);
				Session.set("dirty_slaves_cunt", response);
			});
			return Session.get("dirty_slaves_cunt");
		},
		clean: function() {
			Session.setDefault("clean_slaves_cunt", 0);
			Meteor.call('getCleanSlavesCunt', function(error, response) {
				// console.log("ERROR", error, "RESPONSE", response);
				Session.set("clean_slaves_cunt", response);
			});
			return Session.get("clean_slaves_cunt");
		},
	});

	// whenever #showMoreResults becomes visible, retieve more results
	function loadMore() {
		var threshold, target = $("body");
		if(!target.length) return;

		threshold = $(window).scrollTop() + $(window).height() - target.height();

		// target.height() calculate the height of overall document body
		// window height() calculate the height of browser client area (document), substracted to remove the gap from top edge scrollbar to bottom of document body height
		var distance = target.height() - $(window).height();

		// console.log("DIST:"+distance+
		// 	" BODY:"+  target.height() +
		// 	" SCROLLTOP:"+$(window).scrollTop() +
		// 	" WINDOW:"+ $(window).height()+
		// 	" OFFSET:"+ target.offset().top+
		// 	" DOC:" + $(document).height()+
		// 	" POINT:" + ($(document).height() - $(window).height())
		// 	);

		// if(target.offset().top < threshold) {
		// 	console.log("TRIGGER LOAD");
		// 	Session.set("slaves_limit", Session.get("slaves_limit") + slaves_expansion);
		// }
		// else {
		// 	if(target.data("visible")) {
		// 		target.data("visible", false);
		// 	}
		// }

		if($(window).scrollTop() == $(document).height() - $(window).height()) {
			Session.set("slaves_limit", Session.get("slaves_limit") + slaves_expansion);
		}
	}

	Template.injection.events({
		"submit .new-slave": function(event) {
			event.preventDefault();
			var val = event.target.text.value;
			Meteor.call("addSlave", val);
			event.target.text.value = "";
		},
	});

	$(window).on('beforeunload', function() {
		$(window).scrollTop(0);
	});

	Meteor.startup(function () {
		// code to run on client at startup
		$(window).scroll(loadMore);
	});

	// Template.anal.onRendered(function() {
	// 	this.$(".fertility-dropdown").dropdown();
	// });

	Template.anal.events({
		"mousedown .stock-item": function(event) {
			var stock_id = $(event.target).find("input[name=stockid]").val();
			// var target_element = $(event.target).find("input[name=stockid][value=" + stock_id + "]");
			// console.log(target_element);
			// $("
xxxxxxxxxxxxxxxxx
").appendTo(event.target);
			var target_element = $(event.target).find(".stock-control");
			target_element.transition({
				animation: 'drop',
				duration: '100ms',
				queue: true,
			});
			if(this.virgin == 1) {
				$(event.target).find(".virginity-checkbox").checkbox("set checked");
			}
			else {
				$(event.target).find(".virginity-checkbox").checkbox();
			}
			$(event.target).find(".fertility-dropdown").dropdown();
			$(event.target).find(".status-dropdown").dropdown();
		},
		"mouseenter .virginity-checkbox": function(event) {
		},
		"change .virginity-checkbox": function(event) {
			var virginity = $(event.target).prop('checked') ? '1' : '0';
			// prevent update request happens despite same value, this occurs due to on position switch being applied when clicking-to-expand item
			if(virginity == this.virgin) {
				return;
			}
			data = {
				virgin: virginity,
			};
			Meteor.call("updateVirginity", this._id, data, function(error, response) {
				if(error) {
					console.log(error);
				}
				else {
					showNotification("Virginity lost", "none");
				}
			});
		},
		// "mouseenter .fertility-dropdown": function(event) {
		// 	$(event.target).dropdown();
		// },
		"change .fertility-input": function(event) {
			data = {
				fertility: event.target.value,
			};
			Meteor.call("updateFertility", this._id, data, function(error, response) {
				if(error) {
					console.log(error);
				}
				else {
					showNotification("Womb fertilized", "none");
				}
			});
		},
		// "mouseenter .status-dropdown": function(event) {
		// 	$(event.target).dropdown();
		// },
		"change .status-input": function(event) {
			data = {
				status: event.target.value,
			}
			Meteor.call("updateStatus", this._id, data, function(error, response) {
				if(error) {
					console.log(error);
				}
				else {
					showNotification("Status updated", "none");
				}
			});
		},
	});

	function showNotification(header_message, content_message) {
		var message = {
			header: header_message,
			content: new Date(),
		};
		Session.set("notification", message);
		$(".notification")
			.transition({
				animation: 'scale',
				duration: '0.3s',
			})
			.transition({
				allowRepeats: true,
				animation: 'pulse',
				duration: '2s',
			})
			.transition({
				animation: 'scale',
				duration: '1.5s',
			})
		;
	}

	Template.notification.helpers({
		header: function() {
			Session.setDefault("notification", {header: "idle", content: "no content"});
			return Session.get("notification").header;
		},
		content: function() {
			Session.setDefault("notification", {header: "idle", content: "no content"});
			return Session.get("notification").content;
		},
	});

	// Tracker.autorun(function() {
	// 	var notification = Session.get("notification");
	// 	console.log("CHANGE", notification);
	// });

}
<|endoftext|>