<commit_before>var express = require('express');
var request = require('request');
var cheerio = require('cheerio');
var router = express.Router();

var mongoose = require('mongoose');

var parseAnswerFromMouseoverHandler = function($, $clue, startString){
	if (!$clue.find) {
		return;
	}
	var mouseoverHandler = $clue.find("div").first().attr("onmouseover");
	var startOfAnswer = mouseoverHandler.indexOf(startString);
	var endOfAnswer = mouseoverHandler.indexOf("");
	var answer = mouseoverHandler.slice(startOfAnswer + startString.length, endOfAnswer);
	answer = $("
" + answer + "
").text(); //some answers have html for display in them, we don't want that
	return answer
};

var parseJeopardySubgame = function($, jeopardyDiv, dollarAmountDelta){
	var game = {};
	var categoryOrder = {};
	game.categories = [];
	//parse categories
	jeopardyDiv.find(".category_name").each(function(index, element){
		var categoryTitle = element.children[0].data;
		var newCategory = {
			name: categoryTitle,
			clues: []
		};
		game.categories.push(newCategory);
		categoryOrder[index] = categoryTitle;
	});

	jeopardyDiv.find(".clue").each(function(index, element){
		var value = $(element).find(".clue_value").text().trim();
		var isDailyDouble = false;

		var columnIndex = index % 6;
		// if no value, it's a daily double. We can use gameName and the previous clue's value to figure it out (daily double will never be first in a category)
		if (!value) {
			var numberOfClues = game.categories[columnIndex].clues.length;
			var previousClue = game.categories[columnIndex].clues[numberOfClues - 1];
			var previousValue = 0;
			if (!previousClue) { // lowest row in the column
				if (columnIndex === 0 ) {
					return; //FIXFIX:  daily double in first row, first column
				}

				previousClue = game.categories[columnIndex - 1].clues[numberOfClues];

				if (!previousClue) {
					return;
				}

				value = previousClue.value.slice(1);
			} else {
				previousValue = previousClue.value;
				previousValue = previousValue.slice(1);
				value = parseInt(previousValue) + dollarAmountDelta;

			}

			isDailyDouble = true;
			value = "$" + value;
		}

		var question = $(element).find(".clue_text").text().trim();

		if (!question) { // some games are just missing data
			return;
		}

		//j-archive shows the answer via an onmouseover handler, we need to parse it out to figure out the actual answer
		var answer = parseAnswerFromMouseoverHandler($, $(element), '"correct_response">');

		if (!answer) {
			return;
		}

		var fullQuestionObject = {
			value: value,
			question: question,
			answer: answer,
			isDailyDouble: isDailyDouble
		};

		game.categories[columnIndex].clues.push(fullQuestionObject);

	});

	return game;
};

var parseFinalJeopardy = function($){
	var result = {};
	var categoryName = $(".final_round .category").text().trim();
	var question = $(".final_round .clue_text").text().trim();
	var answer = parseAnswerFromMouseoverHandler($(".final_round .category"), '\\"correct_response\\">');
	return {
		categories: [{
			name: categoryName,
			question: question,
			answer: answer
		}]
	};
}

var parseJeopardyGame = function(html){
	var $ = cheerio.load(html);
	var gameTitle = $("#game_title").text().trim();
	var titleTokens = gameTitle.split("-");

	var isNewValueGame = $("#double_jeopardy_round:contains($2000)").length > 0;
	var dollarAmountDelta;

	if (isNewValueGame) {
		dollarAmountDelta = 200;
	} else {
		dollarAmountDelta = 100;
	}

	var jeopardy = parseJeopardySubgame($, $("#jeopardy_round"), dollarAmountDelta);
	var doubleJeopardy = parseJeopardySubgame($, $("#double_jeopardy_round"), 2 * dollarAmountDelta);
	var finalJeopardy = parseFinalJeopardy($);

	return {
		"show_number": titleTokens[0].trim(),
		"air_date": titleTokens[1].trim(),
		"jeopardy": jeopardy,
		"double_jeopardy": doubleJeopardy,
		"final_jeopardy": finalJeopardy
	};
}

//
// List of Games per season
//

var parseJeopardySeason = function(html) {
	var $ = cheerio.load(html);

	var seasonsTable = $("#content table");
	var games = [];

	seasonsTable.find("tr").each(function(index, element) {
		var newGame = {};
		var gameIdParam = "?game_id=";
		var url = $(element).find("td a").first().attr("href");
		var gameIdIndex = url.lastIndexOf(gameIdParam);
		newGame.id = url.slice(gameIdIndex + gameIdParam.length);
		newGame.displayName = $(element).find("td").first().text().trim();
		games.push(newGame);
	});

	return {
		games: games
	};
};

//
// List of Seasons (top-level menu)
//

var parseSeasonList = function(html) {
	var $ = cheerio.load(html)

	var seasonsTable = $("#content table");
	var seasons = [];

	seasonsTable.find("tr").each(function(index, element) {
		var newSeason = {};
		var seasonIdParam = "?season=";
		var url = $(element).find("td a").first().attr("href");
		var gameIdIndex = url.lastIndexOf(seasonIdParam);
		newSeason.id = url.slice(gameIdIndex + seasonIdParam.length);
		newSeason.displayName = $(element).find("td").first().text().trim();
		seasons.push(newSeason);
	});

	return {
		seasons: seasons
	};

};

router.get('/games/:id', function(req, res) {

	var id;
	if (!req.params.id) {
		id = "5002";
	} else {
		id = req.params.id;
	}

	var url = "http://www.j-archive.com/showgame.php?game_id=" + id;

	request(url, function(error, response, html){
		if(!error){
			res.json(parseJeopardyGame(html));
		}
		else{
			console.log(error);
		}

	});
});

router.get("/seasons/:id", function(req, res) {
	var url = "http://www.j-archive.com/showseason.php?season=" + req.params.id;

	request(url, function(error, response, html) {
		if (!error) {
			res.json(parseJeopardySeason(html));
		} else {
			console.log(error);
		}
	});
});

router.get("/", function(req, res) {
	var url = "http://j-archive.com/listseasons.php";

	request(url, function(error, response, html) {
		if (!error) {
			res.json(parseSeasonList(html));
		} else {
			console.log(error);
		}
	});

});

module.exports = router;<commit_msg>Parse any links in the clue text into media objects for the client<commit_after>var express = require('express');
var request = require('request');
var cheerio = require('cheerio');
var router = express.Router();

var mongoose = require('mongoose');

var parseAnswerFromMouseoverHandler = function($, $clue, startString) {
	if (!$clue.find) {
		return;
	}
	var mouseoverHandler = $clue.find("div").first().attr("onmouseover");
	var startOfAnswer = mouseoverHandler.indexOf(startString);
	var endOfAnswer = mouseoverHandler.indexOf("");
	var answer = mouseoverHandler.slice(startOfAnswer + startString.length, endOfAnswer);
	answer = $("
" + answer + "
").text(); //some answers have html for display in them, we don't want that
	return answer
};

var parseMedia = function($, $clue) {
	var media = [];

	$clue.find(".clue_text a").each(function(index, element) {
		var mediaObject = {};
		mediaObject.url = $(element).attr("href");

		var extension = mediaObject.url.slice(mediaObject.url.lastIndexOf(".") + 1);
		var MEDIA_TYPES = {
			"png": "image",
			"jpg": "image",
			"mp3": "audio"
		}
		var type = MEDIA_TYPES[extension];

		if (!type) 	{
			return;
		}

		mediaObject.type = type;

		media.push(mediaObject);
	});

	return media;
};

var parseJeopardySubgame = function($, jeopardyDiv, dollarAmountDelta){
	var game = {};
	var categoryOrder = {};
	game.categories = [];
	//parse categories
	jeopardyDiv.find(".category_name").each(function(index, element){
		var categoryTitle = $(element).text().trim();
		var newCategory = {
			name: categoryTitle,
			clues: []
		};
		game.categories.push(newCategory);
		categoryOrder[index] = categoryTitle;
	});

	jeopardyDiv.find(".clue").each(function(index, element){
		var value = $(element).find(".clue_value").text().trim();
		var isDailyDouble = false;

		var columnIndex = index % 6;
		// if no value, it's a daily double. We can use gameName and the previous clue's value to figure it out (daily double will never be first in a category)
		if (!value) {
			var numberOfClues = game.categories[columnIndex].clues.length;
			var previousClue = game.categories[columnIndex].clues[numberOfClues - 1];
			var previousValue = 0;
			if (!previousClue) { // lowest row in the column
				if (columnIndex === 0 ) {
					return; //FIXFIX:  daily double in first row, first column
				}

				previousClue = game.categories[columnIndex - 1].clues[numberOfClues];

				if (!previousClue) {
					return;
				}

				value = previousClue.value.slice(1);
			} else {
				previousValue = previousClue.value;
				previousValue = previousValue.slice(1);
				value = parseInt(previousValue) + dollarAmountDelta;

			}

			isDailyDouble = true;
			value = "$" + value;
		}

		var question = $(element).find(".clue_text").text().trim();

		if (!question) { // some games are just missing data
			return;
		}

		//j-archive shows the answer via an onmouseover handler, we need to parse it out to figure out the actual answer
		var answer = parseAnswerFromMouseoverHandler($, $(element), '"correct_response">');

		if (!answer) {
			return;
		}

		var media = parseMedia($, $(element));

		var fullQuestionObject = {
			value: value,
			question: question,
			answer: answer,
			media: media,
			isDailyDouble: isDailyDouble
		};

		game.categories[columnIndex].clues.push(fullQuestionObject);

	});

	return game;
};

var parseFinalJeopardy = function($){
	var result = {};
	var categoryName = $(".final_round .category").text().trim();
	var question = $(".final_round .clue_text").text().trim();
	var answer = parseAnswerFromMouseoverHandler($, $(".final_round .category"), '\\"correct_response\\">');
	return {
		categories: [{
			name: categoryName,
			question: question,
			answer: answer
		}]
	};
}

var parseJeopardyGame = function(html){
	var $ = cheerio.load(html);
	var gameTitle = $("#game_title").text().trim();
	var titleTokens = gameTitle.split("-");

	var isNewValueGame = $("#double_jeopardy_round:contains($2000)").length > 0;
	var dollarAmountDelta;

	if (isNewValueGame) {
		dollarAmountDelta = 200;
	} else {
		dollarAmountDelta = 100;
	}

	var jeopardy = parseJeopardySubgame($, $("#jeopardy_round"), dollarAmountDelta);
	var doubleJeopardy = parseJeopardySubgame($, $("#double_jeopardy_round"), 2 * dollarAmountDelta);
	var finalJeopardy = parseFinalJeopardy($);

	return {
		"show_number": titleTokens[0].trim(),
		"air_date": titleTokens[1].trim(),
		"jeopardy": jeopardy,
		"double_jeopardy": doubleJeopardy,
		"final_jeopardy": finalJeopardy
	};
}

//
// List of Games per season
//

var parseJeopardySeason = function(html) {
	var $ = cheerio.load(html);

	var seasonsTable = $("#content table");
	var games = [];

	seasonsTable.find("tr").each(function(index, element) {
		var newGame = {};
		var gameIdParam = "?game_id=";
		var url = $(element).find("td a").first().attr("href");
		var gameIdIndex = url.lastIndexOf(gameIdParam);
		newGame.id = url.slice(gameIdIndex + gameIdParam.length);
		newGame.displayName = $(element).find("td").first().text().trim();
		games.push(newGame);
	});

	return {
		games: games
	};
};

//
// List of Seasons (top-level menu)
//

var parseSeasonList = function(html) {
	var $ = cheerio.load(html)

	var seasonsTable = $("#content table");
	var seasons = [];

	seasonsTable.find("tr").each(function(index, element) {
		var newSeason = {};
		var seasonIdParam = "?season=";
		var url = $(element).find("td a").first().attr("href");
		var gameIdIndex = url.lastIndexOf(seasonIdParam);
		newSeason.id = url.slice(gameIdIndex + seasonIdParam.length);
		newSeason.displayName = $(element).find("td").first().text().trim();
		seasons.push(newSeason);
	});

	return {
		seasons: seasons
	};

};

router.get('/games/:id', function(req, res) {

	var id;
	if (!req.params.id) {
		id = "5002";
	} else {
		id = req.params.id;
	}

	var url = "http://www.j-archive.com/showgame.php?game_id=" + id;

	request(url, function(error, response, html){
		if(!error){
			res.json(parseJeopardyGame(html));
		}
		else{
			console.log(error);
		}

	});
});

router.get("/seasons/:id", function(req, res) {
	var url = "http://www.j-archive.com/showseason.php?season=" + req.params.id;

	request(url, function(error, response, html) {
		if (!error) {
			res.json(parseJeopardySeason(html));
		} else {
			console.log(error);
		}
	});
});

router.get("/", function(req, res) {
	var url = "http://j-archive.com/listseasons.php";

	request(url, function(error, response, html) {
		if (!error) {
			res.json(parseSeasonList(html));
		} else {
			console.log(error);
		}
	});

});

module.exports = router;<|endoftext|>


<commit_before>/**
 * Re-invent the SVG rendering wheel that the browser already does for us, but
 * this time allow more options and flexibility. For example, we may want to
 * slow down, chop up, reverse, or apply who-knows-what effect to the process
 * of rendering our image.
 *
 * Canvas API. To re-render the XML data presented as SVG, we'll utilize the
 * Canvas API.
 *
 * High-level plans:
 * @TODO: get the very basics of re-rendering SVG protocol, via canvas.
 * @TODO: code for the 'options' object.
 *
 * @note Almost all understanding of SVG specification comes from w3.org
 * documentation.
 * @see http://www.w3.org/TR/SVG
 */
var svgToCanvas = {
  /**
   * Initialize storage build methods needed to use this library.
   *
   * @param object svgNode
   *   The <svg> node, as found somewhere in the DOM tree of our page.
   * @param object canvasNode
   *   The <canvash> node, as found somewhere in the DOM tree, on which we
   *   should be rendering our 2d representation of svgNode
   *   @note: don't pass the canvas "context" (ie.: don't call getContext()).
   * @param object config
   *     @TODO: code this:
   *   - preCompiled <object>: The object this.compileSVGPath() (and other
   *     JSON/parsing functions would have generated), allowing our library to
   *     do the one thing it wants: play with the Canvas API (ie.: make calls
   *     to this.applyPathCommand()).
   *     @note This will allow us to run svgToCanvas compile/parse-methods on
   *     server-side (with the help of DOM libraries, of course), simply
   *     producing JSON files to consume, here.
   *     @TODO: code this:
   *   - reverse <bool>: Render the image in reverse order
   *   - speed <int>: milliseconds specifying how slowly each path should
   *     render.
   *     @TODO: code this:
   *   - effects <object>: Various special effects to change the normal
   *     rendering process.
   *     - sketchy <bool>: get the paths to vary speeds in their rendering,
   *       sometimes back-tracking (erasing?) to look as if a person is drawing
   *       this, live.
   */
  mapToCanvas: function (svgNode, canvasNode, config) {
    //sanity check
    if (svgNode.nodeName.toLowerCase() != 'svg' ||
        canvasNode.nodeName.toLowerCase() != 'canvas') {
      console.error('SVG or Canvas nodes passed incorrectly.');
      return null;
    }

    this.svg = svgNode;
    this.canvas = canvasNode;
    this.config = config;

    //
    //expose everything
    //
    return this;
  },

  /**
   * Render, in canvas calling all necessary internal methods to map our SVG
   * data to Canvas APIs. This is the public method that should be called by
   * end users, after they've received a mapper object from this.mapToCanvas().
   *
   * @note the only committed, Public API
   */
  renderToCanvas: function () {
    //
    //basic rendering for each path
    //
    var paths = this.svg.getElementsByTagName('path');
    for (var i in paths) {
      //for some reason we atually hit the 'length' property.
      if (typeof paths[i] === 'object' &&
          'nodeName' in paths[i] && paths[i].nodeName == 'path') {
        this.renderPath(paths[i]);
      }
    }

    //@TODO: code, then call methods to take the rest of our <svg> object into
    //consideration
  },

  /**
   * Render a particular <path>, on our DOM's canvas.
   *
   * @TODO: either rename compileSVGPath to do more, or code calls to take care
   * of all other attributes Canvas API also covers, eg.: fillStyle().
   *
   * @param object pathNode
   *   The <path> node, as found somewhere in the DOM tree of SVG data.
   */
  renderPath: function (pathNode) {
    var dAttr, styles, data;
    dAttr = pathNode.getAttribute('d');
    styles = { /* @TODO: style-[x] attributes on this <path> node */ };
    data = this.compileSVGPath(dAttr, styles);
    this.applyPathCommand(data.commands);

    //@TODO: do something with this data
    console.error('vaporware: Nothing Coded for "style" attributes!! (styles: %s)\n',
        data.styles);
  },

  /**
   * @TODO: this should be a private method of svgToCanvas.renderPath()
   *
   * Actually break down a given svg path, based on possible SVG "commands" and
   * styles.
   *
   * @param string dAttr
   *   The raw string for a given <path>.
   * @param object styles
   *   The strings from style-related attributes found in <path> node.
   *   - @TODO: document this object, when you start coding it.
   * @return Object
   *   - styles: Object
   *     Any path-wide styles found in <path> attributes.
   *   - commands: Array
   *     Collection of objects representing each of the SVG API "commands" that
   *     were called from a given "d" attribute of a parsed <path> element and
   *     the relevant data following that commmand.
   *     - command <String>: a valid SVG "command"
   *     @see this.svgPathCommands
   *     - data <Array>: typically 0 or more floats, usually representing X/Y
   *     axis coordinates.
   */
  compileSVGPath: function (dAttr, styles) {
    //
    //gather flags and data found in [d] attribute.
    //
    var flags = dAttr.match(this.spec.commandsRegex());
    var data = dAttr.split(this.spec.commandsRegex());
    data.shift();
    data = data.map(function (str) {
      return str.trim();
    });

    //sanity check
    if (data.length != flags.length) {
      console.error('Length of flags in attribute not equal to length of data!');
    }

    /**
     * Compile our d-attribute data into an array in the order it was found.
     */
    var compileDAttr = function (keys, values) {
      var objectified = [];
      for (var i in values) {
        objectified.push({
          command: keys[i],
          data: values[i].split(','),
        });
      }
      return objectified;
    }

    /**
     * Gather style attributes we want to use from <path>, into a useful
     * object.
     */
    var compileStyles = function () {
      return null; //@TODO: append other path-wide attributes onto this array.
    }

    /**
     * Assume numeric element indexes match between the 'values' Array and the
     * 'keys' Array.
     */
    return {
      commands: compileDAttr(flags, data),
      styles: compileStyles(styles),
    };
  },

  /**
   * @TODO: these should be a private property of our object of svgToCanvas
   *
   * SVG "commands", as found in [d] attribute of a <path> element, according
   * to W3G specification.
   *
   * @see this.applyPathCommand()
   */
  spec: {
    commands: 'mzlhvcsqta',
    commandsRegex: function () {
      var regex = '[';
      regex += this.commands; //@TODO: make sure this works!
      regex += ']';
      return (new RegExp(regex, 'ig'));
    }
  },

  /**
   * @TODO: this should be a private method of svgToCanvas.renderPath()
   *
   * Actual "mapping" of our Mapper, calling valid Canvas API methods for each
   * SVG "command" that's called.
   *
   * @see http://www.w3.org/TR/SVG/paths.html
   *
   * @param Array pathData
   *   @see this.compileSVGPath().commands
   */
  applyPathCommand: function(pathData) {
    /**
     * Call the correct Canvas API.
     */
    var lib = this;
    var applyCommand = function (command, data) {
      if (!command.match(lib.spec.commandsRegex())) {
        console.warn('Unknown SVG command found in [d] attribute of svg node id="%s".\n',
            lib.svg.id);
      }

      switch (command) {
        /**
         * Start a new sub-path at the given (x,y) coordinate. M (uppercase)
         * indicates that absolute coordinates will follow; m (lowercase)
         * indicates that relative coordinates will follow. If a moveto is
         * followed by multiple pairs of coordinates, the subsequent pairs are
         * treated as implicit lineto commands. Hence, implicit lineto commands
         * will be relative if the moveto is relative, and absolute if the
         * moveto is absolute. If a relative moveto (m) appears as the first
         * element of the path, then it is treated as a pair of absolute
         * coordinates. In this case, subsequent pairs of coordinates are
         * treated as relative even though the initial moveto is interpreted as
         * an absolute moveto.
         */
        //absolute moveto
        case 'M':
          console.error('vaporware: Absolute "moveto"-command not yet implemented'); //@TODO: code this
          break;
        //relative moveto
        case 'm':
          console.error('vaporware: Relative "moveto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Close the current subpath by drawing a straight line from the
         * current point to current subpath's initial point. Since the Z and z
         * commands take no parameters, they have an identical effect.
         */
        //closepath
        case 'Z':
        case 'z':
          console.error('vaporware: "closepath"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draw a line from the current point to the given (x,y) coordinate
         * which becomes the new current point. L (uppercase) indicates that
         * absolute coordinates will follow; l (lowercase) indicates that
         * relative coordinates will follow. A number of coordinates pairs may
         * be specified to draw a polyline. At the end of the command, the new
         * current point is set to the final set of coordinates provided.
         */
        //absolute lineto
        case 'L':
          console.error('vaporware: Absolute "lineto"-command not yet implemented'); //@TODO: code this
          break;
        //relative lineto
        case 'l':
          console.error('vaporware: Relative "lineto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draws a horizontal line from the current point (cpx, cpy) to (x,
         * cpy). H (uppercase) indicates that absolute coordinates will follow;
         * h (lowercase) indicates that relative coordinates will follow.
         * Multiple x values can be provided (although usually this doesn't
         * make sense). At the end of the command, the new current point
         * becomes (x, cpy) for the final value of x.
         */
        //absolute horizontal lineto
        case 'H':
          console.error('vaporware: Absolute "lineto"-command not yet implemented'); //@TODO: code this
          break;
        //relative horizontal lineto
        case 'h':
          console.error('vaporware: Relative "lineto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draws a vertical line from the current point (cpx, cpy) to (cpx, y).
         * V (uppercase) indicates that absolute coordinates will follow; v
         * (lowercase) indicates that relative coordinates will follow.
         * Multiple y values can be provided (although usually this doesn't
         * make sense). At the end of the command, the new current point
         * becomes (cpx, y) for the final value of y.
         */
        //absolute vertical lineto
        case 'V':
          console.error('vaporware: Absolute "vertical lineto"-command not yet implemented'); //@TODO: code this
          break;
        //relative vertical lineto
        case 'v':
          console.error('vaporware: Relative "vertical lineto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draws a cubic Bézier curve from the current point to (x,y) using
         * (x1,y1) as the control point at the beginning of the curve and
         * (x2,y2) as the control point at the end of the curve. C (uppercase)
         * indicates that absolute coordinates will follow; c (lowercase)
         * indicates that relative coordinates will follow. Multiple sets of
         * coordinates may be specified to draw a polybézier. At the end of the
         * command, the new current point becomes the final (x,y) coordinate
         * pair used in the polybézier.
         */
        //absolute curveto
        case 'C':
          console.error('vaporware: Absolute "curveto"-command not yet implemented'); //@TODO: code this
          break;
        //relative curveto
        case 'c':
          console.error('vaporware: Relative "curveto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draws a cubic Bézier curve from the current point to (x,y). The
         * first control point is assumed to be the reflection of the second
         * control point on the previous command relative to the current point.
         * (If there is no previous command or if the previous command was not
         * an C, c, S or s, assume the first control point is coincident with
         * the current point.) (x2,y2) is the second control point (i.e., the
         * control point at the end of the curve). S (uppercase) indicates that
         * absolute coordinates will follow; s (lowercase) indicates that
         * relative coordinates will follow. Multiple sets of coordinates may
         * be specified to draw a polybézier. At the end of the command, the
         * new current point becomes the final (x,y) coordinate pair used in
         * the polybézier.
         */
        //absolute shorthand/smooth curveto
        case 'S':
          console.error('vaporware: Absolute "shorthand/smooth curveto"-command not yet implemented'); //@TODO: code this
          break;
        //relative shorthand/smooth curveto
        case 's':
          console.error('vaporware: Relative "shorthand/smooth curveto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draws a quadratic Bézier curve from the current point to (x,y) using
         * (x1,y1) as the control point. Q (uppercase) indicates that absolute
         * coordinates will follow; q (lowercase) indicates that relative
         * coordinates will follow. Multiple sets of coordinates may be
         * specified to draw a polybézier. At the end of the command, the new
         * current point becomes the final (x,y) coordinate pair used in the
         * polybézier.
         */
        //absolute quadratic Bézier curveto
        case 'Q':
          console.error('vaporware: Absolute "quadtratic Bézier curveto"-command not yet implemented'); //@TODO: code this
          break;
        //relative quadratic Bézier curveto
        case 'q':
          console.error('vaporware: Relative "quadtratic Bézier curveto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draws a quadratic Bézier curve from the current point to (x,y). The
         * control point is assumed to be the reflection of the control point
         * on the previous command relative to the current point. (If there is
         * no previous command or if the previous command was not a Q, q, T or
         * t, assume the control point is coincident with the current point.) T
         * (uppercase) indicates that absolute coordinates will follow; t
         * (lowercase) indicates that relative coordinates will follow. At the
         * end of the command, the new current point becomes the final (x,y)
         * coordinate pair used in the polybézier.
         */
        //absolute Shorthand/smooth quadratic Bézier curveto
        case 'T':
          console.error('vaporware: Absolute "Shorthand/smooth quadratic Bézier curveto"-command not yet implemented'); //@TODO: code this
          break;
        //relative Shorthand/smooth quadratic Bézier curveto
        case 't':
          console.error('vaporware: Relative "Shorthand/smooth quadratic Bézier curveto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draws an elliptical arc from the current point to (x, y). The size
         * and orientation of the ellipse are defined by two radii (rx, ry) and
         * an x-axis-rotation, which indicates how the ellipse as a whole is
         * rotated relative to the current coordinate system. The center (cx,
         * cy) of the ellipse is calculated automatically to satisfy the
         * constraints imposed by the other parameters. large-arc-flag and
         * sweep-flag contribute to the automatic calculations and help
         * determine how the arc is drawn.
         */
        //absolute elliptical arc
        case 'A':
          console.error('vaporware: Absolute "elliptical arc"-command not yet implemented'); //@TODO: code this
          break;
        //relative elliptical arc
        case 'a':
          console.error('vaporware: Relative "elliptical arc"-command not yet implemented'); //@TODO: code this
          break;
      }
    }

    for (var i in pathData) {
      applyCommand(pathData[i].command, pathData[i].data);
    }
  }
};

<commit_msg>Correction, not the only "public" API.<commit_after>/**
 * Re-invent the SVG rendering wheel that the browser already does for us, but
 * this time allow more options and flexibility. For example, we may want to
 * slow down, chop up, reverse, or apply who-knows-what effect to the process
 * of rendering our image.
 *
 * Canvas API. To re-render the XML data presented as SVG, we'll utilize the
 * Canvas API.
 *
 * High-level plans:
 * @TODO: get the very basics of re-rendering SVG protocol, via canvas.
 * @TODO: code for the 'options' object.
 *
 * @note Almost all understanding of SVG specification comes from w3.org
 * documentation.
 * @see http://www.w3.org/TR/SVG
 */
var svgToCanvas = {
  /**
   * Initialize storage build methods needed to use this library.
   *
   * @note Public: accessing this API is encouraged.
   *
   * @param object svgNode
   *   The <svg> node, as found somewhere in the DOM tree of our page.
   * @param object canvasNode
   *   The <canvash> node, as found somewhere in the DOM tree, on which we
   *   should be rendering our 2d representation of svgNode
   *   @note: don't pass the canvas "context" (ie.: don't call getContext()).
   * @param object config
   *     @TODO: code this:
   *   - preCompiled <object>: The object this.compileSVGPath() (and other
   *     JSON/parsing functions would have generated), allowing our library to
   *     do the one thing it wants: play with the Canvas API (ie.: make calls
   *     to this.applyPathCommand()).
   *     @note This will allow us to run svgToCanvas compile/parse-methods on
   *     server-side (with the help of DOM libraries, of course), simply
   *     producing JSON files to consume, here.
   *     @TODO: code this:
   *   - reverse <bool>: Render the image in reverse order
   *   - speed <int>: milliseconds specifying how slowly each path should
   *     render.
   *     @TODO: code this:
   *   - effects <object>: Various special effects to change the normal
   *     rendering process.
   *     - sketchy <bool>: get the paths to vary speeds in their rendering,
   *       sometimes back-tracking (erasing?) to look as if a person is drawing
   *       this, live.
   */
  mapToCanvas: function (svgNode, canvasNode, config) {
    //sanity check
    if (svgNode.nodeName.toLowerCase() != 'svg' ||
        canvasNode.nodeName.toLowerCase() != 'canvas') {
      console.error('SVG or Canvas nodes passed incorrectly.');
      return null;
    }

    this.svg = svgNode;
    this.canvas = canvasNode;
    this.config = config;

    //
    //expose everything
    //
    return this;
  },

  /**
   * Render, in canvas calling all necessary internal methods to map our SVG
   * data to Canvas APIs. This is the public method that should be called by
   * end users, after they've received a mapper object from this.mapToCanvas().
   *
   * @note Public: accessing this API is encouraged.
   */
  renderToCanvas: function () {
    //
    //basic rendering for each path
    //
    var paths = this.svg.getElementsByTagName('path');
    for (var i in paths) {
      //for some reason we atually hit the 'length' property.
      if (typeof paths[i] === 'object' &&
          'nodeName' in paths[i] && paths[i].nodeName == 'path') {
        this.renderPath(paths[i]);
      }
    }

    //@TODO: code, then call methods to take the rest of our <svg> object into
    //consideration
  },

  /**
   * Render a particular <path>, on our DOM's canvas.
   *
   * @TODO: either rename compileSVGPath to do more, or code calls to take care
   * of all other attributes Canvas API also covers, eg.: fillStyle().
   *
   * @param object pathNode
   *   The <path> node, as found somewhere in the DOM tree of SVG data.
   */
  renderPath: function (pathNode) {
    var dAttr, styles, data;
    dAttr = pathNode.getAttribute('d');
    styles = { /* @TODO: style-[x] attributes on this <path> node */ };
    data = this.compileSVGPath(dAttr, styles);
    this.applyPathCommand(data.commands);

    //@TODO: do something with this data
    console.error('vaporware: Nothing Coded for "style" attributes!! (styles: %s)\n',
        data.styles);
  },

  /**
   * @TODO: this should be a private method of svgToCanvas.renderPath()
   *
   * Actually break down a given svg path, based on possible SVG "commands" and
   * styles.
   *
   * @param string dAttr
   *   The raw string for a given <path>.
   * @param object styles
   *   The strings from style-related attributes found in <path> node.
   *   - @TODO: document this object, when you start coding it.
   * @return Object
   *   - styles: Object
   *     Any path-wide styles found in <path> attributes.
   *   - commands: Array
   *     Collection of objects representing each of the SVG API "commands" that
   *     were called from a given "d" attribute of a parsed <path> element and
   *     the relevant data following that commmand.
   *     - command <String>: a valid SVG "command"
   *     @see this.svgPathCommands
   *     - data <Array>: typically 0 or more floats, usually representing X/Y
   *     axis coordinates.
   */
  compileSVGPath: function (dAttr, styles) {
    //
    //gather flags and data found in [d] attribute.
    //
    var flags = dAttr.match(this.spec.commandsRegex());
    var data = dAttr.split(this.spec.commandsRegex());
    data.shift();
    data = data.map(function (str) {
      return str.trim();
    });

    //sanity check
    if (data.length != flags.length) {
      console.error('Length of flags in attribute not equal to length of data!');
    }

    /**
     * Compile our d-attribute data into an array in the order it was found.
     */
    var compileDAttr = function (keys, values) {
      var objectified = [];
      for (var i in values) {
        objectified.push({
          command: keys[i],
          data: values[i].split(','),
        });
      }
      return objectified;
    }

    /**
     * Gather style attributes we want to use from <path>, into a useful
     * object.
     */
    var compileStyles = function () {
      return null; //@TODO: append other path-wide attributes onto this array.
    }

    /**
     * Assume numeric element indexes match between the 'values' Array and the
     * 'keys' Array.
     */
    return {
      commands: compileDAttr(flags, data),
      styles: compileStyles(styles),
    };
  },

  /**
   * @TODO: these should be a private property of our object of svgToCanvas
   *
   * SVG "commands", as found in [d] attribute of a <path> element, according
   * to W3G specification.
   *
   * @see this.applyPathCommand()
   */
  spec: {
    commands: 'mzlhvcsqta',
    commandsRegex: function () {
      var regex = '[';
      regex += this.commands; //@TODO: make sure this works!
      regex += ']';
      return (new RegExp(regex, 'ig'));
    }
  },

  /**
   * @TODO: this should be a private method of svgToCanvas.renderPath()
   *
   * Actual "mapping" of our Mapper, calling valid Canvas API methods for each
   * SVG "command" that's called.
   *
   * @see http://www.w3.org/TR/SVG/paths.html
   *
   * @param Array pathData
   *   @see this.compileSVGPath().commands
   */
  applyPathCommand: function(pathData) {
    /**
     * Call the correct Canvas API.
     */
    var lib = this;
    var applyCommand = function (command, data) {
      if (!command.match(lib.spec.commandsRegex())) {
        console.warn('Unknown SVG command found in [d] attribute of svg node id="%s".\n',
            lib.svg.id);
      }

      switch (command) {
        /**
         * Start a new sub-path at the given (x,y) coordinate. M (uppercase)
         * indicates that absolute coordinates will follow; m (lowercase)
         * indicates that relative coordinates will follow. If a moveto is
         * followed by multiple pairs of coordinates, the subsequent pairs are
         * treated as implicit lineto commands. Hence, implicit lineto commands
         * will be relative if the moveto is relative, and absolute if the
         * moveto is absolute. If a relative moveto (m) appears as the first
         * element of the path, then it is treated as a pair of absolute
         * coordinates. In this case, subsequent pairs of coordinates are
         * treated as relative even though the initial moveto is interpreted as
         * an absolute moveto.
         */
        //absolute moveto
        case 'M':
          console.error('vaporware: Absolute "moveto"-command not yet implemented'); //@TODO: code this
          break;
        //relative moveto
        case 'm':
          console.error('vaporware: Relative "moveto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Close the current subpath by drawing a straight line from the
         * current point to current subpath's initial point. Since the Z and z
         * commands take no parameters, they have an identical effect.
         */
        //closepath
        case 'Z':
        case 'z':
          console.error('vaporware: "closepath"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draw a line from the current point to the given (x,y) coordinate
         * which becomes the new current point. L (uppercase) indicates that
         * absolute coordinates will follow; l (lowercase) indicates that
         * relative coordinates will follow. A number of coordinates pairs may
         * be specified to draw a polyline. At the end of the command, the new
         * current point is set to the final set of coordinates provided.
         */
        //absolute lineto
        case 'L':
          console.error('vaporware: Absolute "lineto"-command not yet implemented'); //@TODO: code this
          break;
        //relative lineto
        case 'l':
          console.error('vaporware: Relative "lineto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draws a horizontal line from the current point (cpx, cpy) to (x,
         * cpy). H (uppercase) indicates that absolute coordinates will follow;
         * h (lowercase) indicates that relative coordinates will follow.
         * Multiple x values can be provided (although usually this doesn't
         * make sense). At the end of the command, the new current point
         * becomes (x, cpy) for the final value of x.
         */
        //absolute horizontal lineto
        case 'H':
          console.error('vaporware: Absolute "lineto"-command not yet implemented'); //@TODO: code this
          break;
        //relative horizontal lineto
        case 'h':
          console.error('vaporware: Relative "lineto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draws a vertical line from the current point (cpx, cpy) to (cpx, y).
         * V (uppercase) indicates that absolute coordinates will follow; v
         * (lowercase) indicates that relative coordinates will follow.
         * Multiple y values can be provided (although usually this doesn't
         * make sense). At the end of the command, the new current point
         * becomes (cpx, y) for the final value of y.
         */
        //absolute vertical lineto
        case 'V':
          console.error('vaporware: Absolute "vertical lineto"-command not yet implemented'); //@TODO: code this
          break;
        //relative vertical lineto
        case 'v':
          console.error('vaporware: Relative "vertical lineto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draws a cubic Bézier curve from the current point to (x,y) using
         * (x1,y1) as the control point at the beginning of the curve and
         * (x2,y2) as the control point at the end of the curve. C (uppercase)
         * indicates that absolute coordinates will follow; c (lowercase)
         * indicates that relative coordinates will follow. Multiple sets of
         * coordinates may be specified to draw a polybézier. At the end of the
         * command, the new current point becomes the final (x,y) coordinate
         * pair used in the polybézier.
         */
        //absolute curveto
        case 'C':
          console.error('vaporware: Absolute "curveto"-command not yet implemented'); //@TODO: code this
          break;
        //relative curveto
        case 'c':
          console.error('vaporware: Relative "curveto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draws a cubic Bézier curve from the current point to (x,y). The
         * first control point is assumed to be the reflection of the second
         * control point on the previous command relative to the current point.
         * (If there is no previous command or if the previous command was not
         * an C, c, S or s, assume the first control point is coincident with
         * the current point.) (x2,y2) is the second control point (i.e., the
         * control point at the end of the curve). S (uppercase) indicates that
         * absolute coordinates will follow; s (lowercase) indicates that
         * relative coordinates will follow. Multiple sets of coordinates may
         * be specified to draw a polybézier. At the end of the command, the
         * new current point becomes the final (x,y) coordinate pair used in
         * the polybézier.
         */
        //absolute shorthand/smooth curveto
        case 'S':
          console.error('vaporware: Absolute "shorthand/smooth curveto"-command not yet implemented'); //@TODO: code this
          break;
        //relative shorthand/smooth curveto
        case 's':
          console.error('vaporware: Relative "shorthand/smooth curveto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draws a quadratic Bézier curve from the current point to (x,y) using
         * (x1,y1) as the control point. Q (uppercase) indicates that absolute
         * coordinates will follow; q (lowercase) indicates that relative
         * coordinates will follow. Multiple sets of coordinates may be
         * specified to draw a polybézier. At the end of the command, the new
         * current point becomes the final (x,y) coordinate pair used in the
         * polybézier.
         */
        //absolute quadratic Bézier curveto
        case 'Q':
          console.error('vaporware: Absolute "quadtratic Bézier curveto"-command not yet implemented'); //@TODO: code this
          break;
        //relative quadratic Bézier curveto
        case 'q':
          console.error('vaporware: Relative "quadtratic Bézier curveto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draws a quadratic Bézier curve from the current point to (x,y). The
         * control point is assumed to be the reflection of the control point
         * on the previous command relative to the current point. (If there is
         * no previous command or if the previous command was not a Q, q, T or
         * t, assume the control point is coincident with the current point.) T
         * (uppercase) indicates that absolute coordinates will follow; t
         * (lowercase) indicates that relative coordinates will follow. At the
         * end of the command, the new current point becomes the final (x,y)
         * coordinate pair used in the polybézier.
         */
        //absolute Shorthand/smooth quadratic Bézier curveto
        case 'T':
          console.error('vaporware: Absolute "Shorthand/smooth quadratic Bézier curveto"-command not yet implemented'); //@TODO: code this
          break;
        //relative Shorthand/smooth quadratic Bézier curveto
        case 't':
          console.error('vaporware: Relative "Shorthand/smooth quadratic Bézier curveto"-command not yet implemented'); //@TODO: code this
          break;

        /**
         * Draws an elliptical arc from the current point to (x, y). The size
         * and orientation of the ellipse are defined by two radii (rx, ry) and
         * an x-axis-rotation, which indicates how the ellipse as a whole is
         * rotated relative to the current coordinate system. The center (cx,
         * cy) of the ellipse is calculated automatically to satisfy the
         * constraints imposed by the other parameters. large-arc-flag and
         * sweep-flag contribute to the automatic calculations and help
         * determine how the arc is drawn.
         */
        //absolute elliptical arc
        case 'A':
          console.error('vaporware: Absolute "elliptical arc"-command not yet implemented'); //@TODO: code this
          break;
        //relative elliptical arc
        case 'a':
          console.error('vaporware: Relative "elliptical arc"-command not yet implemented'); //@TODO: code this
          break;
      }
    }

    for (var i in pathData) {
      applyCommand(pathData[i].command, pathData[i].data);
    }
  }
};

<|endoftext|>


<commit_before>/**
* User interface for managing groups.
* @class
* @name $.boom.group_editor
*/
$.widget( 'boom.group_editor', {
	/** @lends $.boom.group_editor */

	/**
	default config
	@property
	*/
	options: {
		/** URL prefix for ajax calls */
		base_url : '/cms/groups/',
		/** ID of the group being edited */
		id : null
	},

	_create: function() {

		this._bind();
	},

	_bind: function() {

		var self = this;
		var selected_page = null;

		$.boom.loader.hide();

		this.element
		.ui()
		.on( 'change', '#b-group-roles-general input[type=radio]', function( event ){

			var role_id = this.name;
			var allowed = this.value;

			self._change_permissions( role_id, allowed, 0 );

		})
		.on( 'change', '#b-group-roles-pages input[type=radio]', function( event ){

			var role_id = this.name;
			var allowed = parseInt( this.value, 10 );
			var page_id = selected_page;

			self._change_permissions( role_id, allowed, page_id );

		});

		self._check_inputs( $( '#b-group-roles-general input[type=radio]'), '[value="-1"]' );

		$.get( self.options.base_url + 'list_roles/' + self.options.id + '?page_id=0' )
		.done( function( data ){
			for ( role in data ) {

				self._check_inputs( $( 'input[name=' + role + ']' ), '[value=' + data[ role ] + ']' );
			}
		});

		/**
		 * Clicking on a page in the tree.
		 * Should make a GET call to /cms/groups/list_roles/<group ID>?page_id=<page ID>
		 *
		 * This will return a json encoded array of role ID => <value>
		 * Possible values are 1 if the role is allowed and 0 if the role is disallowed.
		 * If nothing is set for a role then that role ID won't be in the returned array.
		 *
		 * The role checkboxes should then be updated if the correct values.
		 */

		var page_tree = this.element.find( '#b-group-roles-pages .boom-tree' );

		$.boom.util.page_tree(  page_tree )
			.progress( function( page ) {

				selected_page = page.page_id;

				self._check_inputs( $( '#b-group-roles-pages input[type=radio]'), '[value="-1"]' );

				page_tree
					.find( 'a[rel=' + page.page_id + ']' )
					.parents( '.boom-tree' )
					.find( 'a.ui-state-active' )
					.removeClass( 'ui-state-active' )
					.end()
					.end()
					.addClass( 'ui-state-active' );

				$.get( self.options.base_url + 'list_roles/' + self.options.id + '?page_id=' + selected_page )
				.done( function( data ){
					for ( role in data ) {

						self._check_inputs( $( 'input[name=' + role + ']' ), '[value=' + data[ role ] + ']' );
					}
				});

			});

	},

	_check_inputs: function( radio_buttons, filter ) {

		return radio_buttons
			.filter( ':checked' )
			.prop( 'checked', false )
			.removeAttr( 'checked' )
			.end()
			.filter( filter )
			.prop( 'checked', true )
			.attr( 'checked', 'checked' );

	},

	_change_permissions: function( role_id, allowed, page_id ) {

		var self = this;

		return $.post(
			self.options.base_url + 'remove_role/' + self.options.id,
			{
				role_id : role_id,
				page_id : page_id
			}
		)
		.pipe( function( response ){
			if ( allowed == -1 ){
				console.log( this );
				return this;
			}
			return $.post(
				self.options. base_url + 'add_role/' + self.options.id,
				{
					role_id : role_id,
					allowed : allowed,
					page_id: page_id
				}
			);
		})
		.done( function( response ){
		});

	}
});<commit_msg>pass selected value when updating radio buttons<commit_after>/**
* User interface for managing groups.
* @class
* @name $.boom.group_editor
*/
$.widget( 'boom.group_editor', {
	/** @lends $.boom.group_editor */

	/**
	default config
	@property
	*/
	options: {
		/** URL prefix for ajax calls */
		base_url : '/cms/groups/',
		/** ID of the group being edited */
		id : null
	},

	_create: function() {

		this._bind();
	},

	_bind: function() {

		var self = this;
		var selected_page = null;

		$.boom.loader.hide();

		this.element
		.ui()
		.on( 'change', '#b-group-roles-general input[type=radio]', function( event ){

			var role_id = this.name;
			var allowed = this.value;

			self._change_permissions( role_id, allowed, 0 );

		})
		.on( 'change', '#b-group-roles-pages input[type=radio]', function( event ){

			var role_id = this.name;
			var allowed = parseInt( this.value, 10 );
			var page_id = selected_page;

			self._change_permissions( role_id, allowed, page_id );

		});

		self._check_inputs( $( '#b-group-roles-general input[type=radio]'), -1 );

		$.get( self.options.base_url + 'list_roles/' + self.options.id + '?page_id=0' )
		.done( function( data ){
			for ( role in data ) {

				self._check_inputs( $( 'input[name=' + role + ']' ), data[ role ] );
			}
		});

		/**
		 * Clicking on a page in the tree.
		 * Should make a GET call to /cms/groups/list_roles/<group ID>?page_id=<page ID>
		 *
		 * This will return a json encoded array of role ID => <value>
		 * Possible values are 1 if the role is allowed and 0 if the role is disallowed.
		 * If nothing is set for a role then that role ID won't be in the returned array.
		 *
		 * The role checkboxes should then be updated if the correct values.
		 */

		var page_tree = this.element.find( '#b-group-roles-pages .boom-tree' );

		$.boom.util.page_tree(  page_tree )
			.progress( function( page ) {

				selected_page = page.page_id;

				self._check_inputs( $( '#b-group-roles-pages input[type=radio]'), -1 );

				page_tree
					.find( 'a[rel=' + page.page_id + ']' )
					.parents( '.boom-tree' )
					.find( 'a.ui-state-active' )
					.removeClass( 'ui-state-active' )
					.end()
					.end()
					.addClass( 'ui-state-active' );

				$.get( self.options.base_url + 'list_roles/' + self.options.id + '?page_id=' + selected_page )
				.done( function( data ){
					for ( role in data ) {

						self._check_inputs( $( 'input[name=' + role + ']' ), data[ role ] );
					}
				});

			});

	},

	_check_inputs: function( radio_buttons, value ) {

		radio_buttons
			.filter( ':checked' )
			.prop( 'checked', false )
			.removeAttr( 'checked' )
			.end()
			.filter( '[value=' + data[ role ] + ']' )
			.prop( 'checked', true )
			.attr( 'checked', 'checked' );

	},

	_change_permissions: function( role_id, allowed, page_id ) {

		var self = this;

		return $.post(
			self.options.base_url + 'remove_role/' + self.options.id,
			{
				role_id : role_id,
				page_id : page_id
			}
		)
		.pipe( function( response ){
			if ( allowed == -1 ){
				console.log( this );
				return this;
			}
			return $.post(
				self.options. base_url + 'add_role/' + self.options.id,
				{
					role_id : role_id,
					allowed : allowed,
					page_id: page_id
				}
			);
		})
		.done( function( response ){
		});

	}
});<|endoftext|>


<commit_before>var Abba = (function(Abba, $, pv) {

Abba.BASELINE_ALPHA = 0.05;

Abba.RESULT_ROW_HTML = ' \

 \
  	 \
  	 \
  	 \
   \
     \
    ±  \
  	 \
  	 \
  	 \
   \
     \
    ±  \
  	 \

';

Abba.RESULT_TABLE_HTML = ' \

 \

 \

 \
            	 \
            Successes	 \
            Total	 \
            Success Rate	 \
            p-value	 \
            Improvement	 \

 \

 \

 \

 \

';

Abba.RESULT_COLORS = {
    neutral: '#D8D8D8',
    lose: '#DF1210',
    win: '#48E000'
};

Abba.Formatter = function() {}
Abba.Formatter.prototype = {
    describeNumber: function(number, decimalSpots) {
        if (!decimalSpots) {
            if (number % 1 === 0) {
                decimalSpots = 0;
            } else {
                decimalSpots = 1;
            }
        }

        var numberString = String(number.toFixed(decimalSpots));
        var parts = numberString.split('.');
        var whole = parts[0];

        var pattern = /(\d+)(\d{3})(,|$)/;
        while (pattern.test(whole)) {
            whole = whole.replace(pattern, '$1,$2');
        }

        numberString = (parts.length > 1) ? whole + '.' + parts[1] : whole;
        return numberString;
    },

    _round: function(value, places) {
        var factor = Math.pow(10, places);
        return Math.round(value * factor) / factor;

    },

    _getDefaultPlaces: function(ratio) {
        if (Math.abs(ratio) < 0.01) {
            return 2;
        } else if (Math.abs(ratio) < 0.1) {
            return 1;
        } else {
            return 0;
        }
    },

    percent: function(ratio, places) {
        if (places === undefined) {
            places = this._getDefaultPlaces(ratio);
        }
        return this._round(100 * ratio, places) + '%';
    },

    formatPValue: function(pValue) {
        if (pValue < 0.0001) {
            return '< 0.01%';
        } else {
            return (100 * pValue).toFixed(this._getDefaultPlaces(pValue)) + '%';
        }
    }
}

Abba.ResultRowView = function($row, formatter) {
    this._$row = $row;
    this._formatter = formatter;
}
Abba.ResultRowView.prototype = {
    _renderInterval: function(valueWithInterval, $container) {
        $container
            .find('.base').text(this._formatter.percent(valueWithInterval.value)).end()
            .find('.error > span').text(this._formatter.percent(valueWithInterval.intervalWidth));
    },

    _supportsSvg: function(document) {
        var div = document.createElement('div');
        div.innerHTML = '<svg/>';
        return (div.firstChild && div.firstChild.namespaceURI) == 'http://www.w3.org/2000/svg';
    },

    renderConversion: function(numSuccesses, numTrials, rate) {
        this._$row
            .find('.yes').text(this._formatter.describeNumber(numSuccesses)).end()
            .find('.total').text(this._formatter.describeNumber(numTrials));
        this._renderInterval(rate, this._$row.find('.conversion-numeric'));
    },

    renderOutcome: function(pValue, improvement) {
        this._$row.find('.p-value').text(this._formatter.formatPValue(pValue));
        this._renderInterval(improvement, this._$row.find('.improvement'));
    },

    blankOutcome: function() {
        this._$row.find('.p-value, .improvement').html('—');
    },

    renderConversionRange: function(range, baselineRange, overallRange) {
        if (!this._supportsSvg(document)) {
            return;
        }

        var canvas = this._$row.find('.conversion-visual');
        var width = 200;
        var height = 25;
        var scale = pv.Scale
            .linear(overallRange.lowerBound, overallRange.upperBound)
            .range(0, width);
        var colors = pv.colors(
            Abba.RESULT_COLORS.neutral,
            Abba.RESULT_COLORS.lose,
            Abba.RESULT_COLORS.win
        );

        var panel = new pv.Panel()
            .width(width)
            .height(height)
            .margin(10)
            .canvas(canvas[0]);

        var ranges = [
            range,
            {
                lowerBound: range.lowerBound,
                upperBound: Math.min(range.upperBound, baselineRange.lowerBound)
            },
            {
                lowerBound: Math.max(range.lowerBound, baselineRange.upperBound),
                upperBound: range.upperBound
            }
        ];

        // Range indicators
        panel.add(pv.Bar)
            .data(ranges)
            .top(0)
            .height(height)
            .left(function(data) { return scale(data.lowerBound); })
            .width(function(data) { return scale(data.upperBound) - scale(data.lowerBound); })
            .fillStyle(colors.by(function() { return this.index; }));

        // End marks
        var endMarkHeight = 7;
        panel.add(pv.Rule)
            .data([overallRange.lowerBound, overallRange.upperBound])
            .visible(function(data) { return data; })
            .left(function(data) { var l = scale(data); return (this.index === 0) ? l - 1 : l; })
            .top(height / 2 - endMarkHeight / 2)
            .height(endMarkHeight)
            .strokeStyle('#444')
            .anchor(function() { return (this.index === 0) ? 'left' : 'right'; })
            .add(pv.Label)
            .font('12px "Arial"')
            .textStyle('#444')
            .textBaseline('middle')
            .text(function() { return (this.index === 0) ? '-' : '+'; });

        // Center line
        panel.add(pv.Rule)
            .left(0)
            .top(height / 2)
            .width(width)
            .strokeStyle('#444');

        panel.render();
    }
};

// Handles all DOM manipulation and event binding
Abba.ResultsView = function($container) {
    this._$container = $container;
    this._formatter = new Abba.Formatter();

    $container.html(Abba.RESULT_TABLE_HTML);
    $container.hide();
}
Abba.ResultsView.prototype = {
    addResultRow: function(label) {
        this._$container.show();

        var $resultTable = this._$container.find('.result-table');
        $resultTable.append(Abba.RESULT_ROW_HTML);
        var $resultRow = $resultTable.children().last();
        $resultRow.find('.bucket-name').text(label);
        return new Abba.ResultRowView($resultRow, this._formatter);
    },

    clearResults: function() {
        this._$container.find('.result-row').remove();
        this._$container.hide();
    }
};

Abba.ResultsPresenter = function(experimentClass) {
    this._view = undefined;
    this._experimentClass = experimentClass;
}
Abba.ResultsPresenter.prototype = {
    bind: function(view) {
        this._view = view;
    },

    _computeResults: function(allInputs) {
        var experiment = new this._experimentClass(allInputs.variations.length,
                                                   allInputs.baseline.numSuccesses,
                                                   allInputs.baseline.numTrials,
                                                   Abba.BASELINE_ALPHA);

        var baselineProportion = experiment.getBaselineProportion();
        var overallConversionBounds = {lowerBound: baselineProportion.range().lowerBound,
                                       upperBound: baselineProportion.range().upperBound};
        var variations = allInputs.variations.map(function(variation) {
            var outcome = experiment.getResults(variation.numSuccesses, variation.numTrials);
            overallConversionBounds.lowerBound = Math.min(overallConversionBounds.lowerBound,
                                                          outcome.proportion.range().lowerBound)
            overallConversionBounds.upperBound = Math.max(overallConversionBounds.upperBound,
                                                          outcome.proportion.range().upperBound)
            return {inputs: variation, outcome: outcome};
        });

        return {
            baselineProportion: baselineProportion,
            overallConversionBounds: overallConversionBounds,
            variations: variations
        };
    },

    computeAndDisplayResults: function(allInputs) {
        this._view.clearResults();
        var results = this._computeResults(allInputs);

        var renderConversionWithRange = function(resultRow, inputs, proportion) {
            resultRow.renderConversion(inputs.numSuccesses, inputs.numTrials, proportion)
            resultRow.renderConversionRange(proportion.range(),
                                            results.baselineProportion.range(),
                                            results.overallConversionBounds);
        };

        var baselineResultRow = this._view.addResultRow(allInputs.baseline.label);
        renderConversionWithRange(baselineResultRow,
                                  allInputs.baseline,
                                  results.baselineProportion);
        baselineResultRow.blankOutcome();

        var self = this;
        results.variations.forEach(function(variation_results) {
            var resultRow = self._view.addResultRow(variation_results.inputs.label);
            renderConversionWithRange(resultRow,
                                      variation_results.inputs,
                                      variation_results.outcome.proportion);
            resultRow.renderOutcome(variation_results.outcome.pValue,
                                    variation_results.outcome.relativeImprovement);
        });
    }
};

Abba.Abba = function(baselineName, baselineNumSuccesses, baselineNumTrials) {
    this._baseline = {
        label: baselineName,
        numSuccesses: baselineNumSuccesses,
        numTrials: baselineNumTrials
    };
    this._variations = [];
}
Abba.Abba.prototype = {
    addVariation: function(label, numSuccesses, numTrials) {
        this._variations.push({label: label, numSuccesses: numSuccesses, numTrials: numTrials});
    },

    renderTo: function(container) {
        var presenter = new Abba.ResultsPresenter(Abba.Experiment);
        presenter.bind(new Abba.ResultsView($(container)));
        presenter.computeAndDisplayResults(
            {baseline: this._baseline, variations: this._variations});
    },

    getResults: function() {
        var experiment = new Abba.Experiment(this._variations.length,
                                             this._baseline.numSuccesses,
                                             this._baseline.numTrials,
                                             Abba.BASELINE_ALPHA);
        results = {}
        results[this._baseline.label] = experiment.getBaselineProportion();
        this._variations.forEach(function(variation) {
            results[variation.label] = experiment.getResults(variation.numSuccesses,
                                                             variation.numTrials);
        });
        return results;
    }
};

return Abba;
}(Abba || {}, jQuery, pv));
<commit_msg>don't format p-values as percentages<commit_after>var Abba = (function(Abba, $, pv) {

Abba.BASELINE_ALPHA = 0.05;

Abba.RESULT_ROW_HTML = ' \

 \
  	 \
  	 \
  	 \
   \
     \
    ±  \
  	 \
  	 \
  	 \
   \
     \
    ±  \
  	 \

';

Abba.RESULT_TABLE_HTML = ' \

 \

 \

 \
            	 \
            Successes	 \
            Total	 \
            Success Rate	 \
            p-value	 \
            Improvement	 \

 \

 \

 \

 \

';

Abba.RESULT_COLORS = {
    neutral: '#D8D8D8',
    lose: '#DF1210',
    win: '#48E000'
};

Abba.Formatter = function() {}
Abba.Formatter.prototype = {
    describeNumber: function(number, decimalSpots) {
        if (!decimalSpots) {
            if (number % 1 === 0) {
                decimalSpots = 0;
            } else {
                decimalSpots = 1;
            }
        }

        var numberString = String(number.toFixed(decimalSpots));
        var parts = numberString.split('.');
        var whole = parts[0];

        var pattern = /(\d+)(\d{3})(,|$)/;
        while (pattern.test(whole)) {
            whole = whole.replace(pattern, '$1,$2');
        }

        numberString = (parts.length > 1) ? whole + '.' + parts[1] : whole;
        return numberString;
    },

    _round: function(value, places) {
        var factor = Math.pow(10, places);
        return Math.round(value * factor) / factor;

    },

    _getDefaultPlaces: function(ratio) {
        if (Math.abs(ratio) < 0.01) {
            return 2;
        } else if (Math.abs(ratio) < 0.1) {
            return 1;
        } else {
            return 0;
        }
    },

    percent: function(ratio, places) {
        if (places === undefined) {
            places = this._getDefaultPlaces(ratio);
        }
        return this._round(100 * ratio, places) + '%';
    },

    formatPValue: function(pValue) {
        if (pValue < 0.0001) {
            return '< 0.0001';
        } else {
            return pValue.toFixed(this._getDefaultPlaces(pValue) + 2);
        }
    }
}

Abba.ResultRowView = function($row, formatter) {
    this._$row = $row;
    this._formatter = formatter;
}
Abba.ResultRowView.prototype = {
    _renderInterval: function(valueWithInterval, $container) {
        $container
            .find('.base').text(this._formatter.percent(valueWithInterval.value)).end()
            .find('.error > span').text(this._formatter.percent(valueWithInterval.intervalWidth));
    },

    _supportsSvg: function(document) {
        var div = document.createElement('div');
        div.innerHTML = '<svg/>';
        return (div.firstChild && div.firstChild.namespaceURI) == 'http://www.w3.org/2000/svg';
    },

    renderConversion: function(numSuccesses, numTrials, rate) {
        this._$row
            .find('.yes').text(this._formatter.describeNumber(numSuccesses)).end()
            .find('.total').text(this._formatter.describeNumber(numTrials));
        this._renderInterval(rate, this._$row.find('.conversion-numeric'));
    },

    renderOutcome: function(pValue, improvement) {
        this._$row.find('.p-value').text(this._formatter.formatPValue(pValue));
        this._renderInterval(improvement, this._$row.find('.improvement'));
    },

    blankOutcome: function() {
        this._$row.find('.p-value, .improvement').html('—');
    },

    renderConversionRange: function(range, baselineRange, overallRange) {
        if (!this._supportsSvg(document)) {
            return;
        }

        var canvas = this._$row.find('.conversion-visual');
        var width = 200;
        var height = 25;
        var scale = pv.Scale
            .linear(overallRange.lowerBound, overallRange.upperBound)
            .range(0, width);
        var colors = pv.colors(
            Abba.RESULT_COLORS.neutral,
            Abba.RESULT_COLORS.lose,
            Abba.RESULT_COLORS.win
        );

        var panel = new pv.Panel()
            .width(width)
            .height(height)
            .margin(10)
            .canvas(canvas[0]);

        var ranges = [
            range,
            {
                lowerBound: range.lowerBound,
                upperBound: Math.min(range.upperBound, baselineRange.lowerBound)
            },
            {
                lowerBound: Math.max(range.lowerBound, baselineRange.upperBound),
                upperBound: range.upperBound
            }
        ];

        // Range indicators
        panel.add(pv.Bar)
            .data(ranges)
            .top(0)
            .height(height)
            .left(function(data) { return scale(data.lowerBound); })
            .width(function(data) { return scale(data.upperBound) - scale(data.lowerBound); })
            .fillStyle(colors.by(function() { return this.index; }));

        // End marks
        var endMarkHeight = 7;
        panel.add(pv.Rule)
            .data([overallRange.lowerBound, overallRange.upperBound])
            .visible(function(data) { return data; })
            .left(function(data) { var l = scale(data); return (this.index === 0) ? l - 1 : l; })
            .top(height / 2 - endMarkHeight / 2)
            .height(endMarkHeight)
            .strokeStyle('#444')
            .anchor(function() { return (this.index === 0) ? 'left' : 'right'; })
            .add(pv.Label)
            .font('12px "Arial"')
            .textStyle('#444')
            .textBaseline('middle')
            .text(function() { return (this.index === 0) ? '-' : '+'; });

        // Center line
        panel.add(pv.Rule)
            .left(0)
            .top(height / 2)
            .width(width)
            .strokeStyle('#444');

        panel.render();
    }
};

// Handles all DOM manipulation and event binding
Abba.ResultsView = function($container) {
    this._$container = $container;
    this._formatter = new Abba.Formatter();

    $container.html(Abba.RESULT_TABLE_HTML);
    $container.hide();
}
Abba.ResultsView.prototype = {
    addResultRow: function(label) {
        this._$container.show();

        var $resultTable = this._$container.find('.result-table');
        $resultTable.append(Abba.RESULT_ROW_HTML);
        var $resultRow = $resultTable.children().last();
        $resultRow.find('.bucket-name').text(label);
        return new Abba.ResultRowView($resultRow, this._formatter);
    },

    clearResults: function() {
        this._$container.find('.result-row').remove();
        this._$container.hide();
    }
};

Abba.ResultsPresenter = function(experimentClass) {
    this._view = undefined;
    this._experimentClass = experimentClass;
}
Abba.ResultsPresenter.prototype = {
    bind: function(view) {
        this._view = view;
    },

    _computeResults: function(allInputs) {
        var experiment = new this._experimentClass(allInputs.variations.length,
                                                   allInputs.baseline.numSuccesses,
                                                   allInputs.baseline.numTrials,
                                                   Abba.BASELINE_ALPHA);

        var baselineProportion = experiment.getBaselineProportion();
        var overallConversionBounds = {lowerBound: baselineProportion.range().lowerBound,
                                       upperBound: baselineProportion.range().upperBound};
        var variations = allInputs.variations.map(function(variation) {
            var outcome = experiment.getResults(variation.numSuccesses, variation.numTrials);
            overallConversionBounds.lowerBound = Math.min(overallConversionBounds.lowerBound,
                                                          outcome.proportion.range().lowerBound)
            overallConversionBounds.upperBound = Math.max(overallConversionBounds.upperBound,
                                                          outcome.proportion.range().upperBound)
            return {inputs: variation, outcome: outcome};
        });

        return {
            baselineProportion: baselineProportion,
            overallConversionBounds: overallConversionBounds,
            variations: variations
        };
    },

    computeAndDisplayResults: function(allInputs) {
        this._view.clearResults();
        var results = this._computeResults(allInputs);

        var renderConversionWithRange = function(resultRow, inputs, proportion) {
            resultRow.renderConversion(inputs.numSuccesses, inputs.numTrials, proportion)
            resultRow.renderConversionRange(proportion.range(),
                                            results.baselineProportion.range(),
                                            results.overallConversionBounds);
        };

        var baselineResultRow = this._view.addResultRow(allInputs.baseline.label);
        renderConversionWithRange(baselineResultRow,
                                  allInputs.baseline,
                                  results.baselineProportion);
        baselineResultRow.blankOutcome();

        var self = this;
        results.variations.forEach(function(variation_results) {
            var resultRow = self._view.addResultRow(variation_results.inputs.label);
            renderConversionWithRange(resultRow,
                                      variation_results.inputs,
                                      variation_results.outcome.proportion);
            resultRow.renderOutcome(variation_results.outcome.pValue,
                                    variation_results.outcome.relativeImprovement);
        });
    }
};

Abba.Abba = function(baselineName, baselineNumSuccesses, baselineNumTrials) {
    this._baseline = {
        label: baselineName,
        numSuccesses: baselineNumSuccesses,
        numTrials: baselineNumTrials
    };
    this._variations = [];
}
Abba.Abba.prototype = {
    addVariation: function(label, numSuccesses, numTrials) {
        this._variations.push({label: label, numSuccesses: numSuccesses, numTrials: numTrials});
    },

    renderTo: function(container) {
        var presenter = new Abba.ResultsPresenter(Abba.Experiment);
        presenter.bind(new Abba.ResultsView($(container)));
        presenter.computeAndDisplayResults(
            {baseline: this._baseline, variations: this._variations});
    },

    getResults: function() {
        var experiment = new Abba.Experiment(this._variations.length,
                                             this._baseline.numSuccesses,
                                             this._baseline.numTrials,
                                             Abba.BASELINE_ALPHA);
        results = {}
        results[this._baseline.label] = experiment.getBaselineProportion();
        this._variations.forEach(function(variation) {
            results[variation.label] = experiment.getResults(variation.numSuccesses,
                                                             variation.numTrials);
        });
        return results;
    }
};

return Abba;
}(Abba || {}, jQuery, pv));
<|endoftext|>


<commit_before>/**
 * Utilitiy functions for working with lists, mainly used by select renderer
 */

import {utils} from 'bunsen-core'
const {ValueWrapper} = utils.path
import Ember from 'ember'
const {Logger, RSVP, get, typeOf} = Ember

const {isArray} = Array
const {keys} = Object

/**
 * set a list's available options
 * @param {String} bunsenId - the bunsen id for this property
 * @param {Object[]} data - initializes the list with this
 * @param {String} filter - the optional string to filter on
 * @param {Object} options - the bunsen model definition
 * @param {Object} store - the ember-data store
 * @param {Object} value - the current value object for the form instance
 * @param {Boolean} keepCurrentValue - determines whether we need to refetch for the current value
 * @returns {RSVP.Promise} a promise that resolves to a list of items
 */
export function getOptions ({ajax, bunsenId, data, filter = '', options, store, value, keepCurrentValue}) {
  // escape regexp metacharacters for local data filtering since filter is not a regular expression
  const safeFilter = filter.replace(/[.?*+^$[\]\\(){}|-]/g, '\\$&')
  const filterRegex = new RegExp(safeFilter, 'i')
  const filteredData = data.filter((item) => filterRegex.test(item.label))

  if (options.modelType) {
    return getItemsFromEmberData(
      {value, modelDef: options, data: filteredData, bunsenId, store, filter, keepCurrentValue})
  } else if (options.endpoint) {
    return getItemsFromAjaxCall({ajax, bunsenId, data: filteredData, filter, options, value})
  } else if (options.recordsPath) {
    return getItemsFromFormValue({bunsenId, data: filteredData, filter, options, value})
  }

  return RSVP.resolve(filteredData)
}

/**
 * Get query with property references replaced by property values.
 * @param {String} bunsenId - bunsen ID of property (used for relative references)
 * @param {String} filter - the partial match query filter to populate
 * @param {Object} query - query to process references in
 * @param {Object} value - the bunsen value for this form
 * @returns {Object} query object with references filled in
 */
export function getQuery ({bunsenId, filter, query, value}) {
  const result = utils.populateQuery(value, query, bunsenId)

  if (typeOf(result) !== 'object') {
    return result
  }

  // replace the special $filter placeholder with the filter value (if it exists)
  keys(result).forEach((key) => {
    const value = result[key]

    if (typeOf(value) === 'string') {
      result[key] = value.replace('$filter', filter)
    }
  })

  return result
}

/**
 * take enum values and make into a collection
 * @param  {String[]} values the values to make into a collection
 * @param {String} filter - filter
 * @returns {Object[]} a list of item objects
 */
export function getEnumValues (values = [], filter = '') {
  const filteredValues = []
  values.filter((value) => {
    if (!filter || value.toLowerCase().indexOf(filter.toLowerCase()) !== -1) {
      filteredValues.push({label: value.toString(), value})
    }
  })
  return filteredValues
}

/**
 * Fetch the list of items from an API endpoint
 * @param {Object} ajax - ember-ajax service
 * @param {Object} bunsenId - the bunsenId for this form
 * @param {Object[]} data - initializes the list with this
 * @param {String} filter - the partial match query filter to populate
 * @param {Object} options - bunsen model for this property plus view renderer options
 * @param {Object} value - the bunsen value for this form
 * @returns {RSVP.Promise} a promise that resolves with the list of items
 */
export function getItemsFromAjaxCall ({ajax, bunsenId, data, filter, options, value}) {
  const query = getQuery({
    bunsenId,
    filter,
    query: options.query,
    value
  })

  const queryString = keys(query)
    .map((key) => `${key}=${query[key]}`)
    .join('&')

  const url = queryString ? `${options.endpoint}?${queryString}` : options.endpoint

  return ajax.request(url)
    .then((result) => {
      const records = options.recordsPath ? get(result, options.recordsPath) : result

      if (!isArray(records)) {
        Logger.warn(
          `Expected an array of records at "${options.recordPath}" but got:`,
          records
        )

        return []
      }

      const {labelAttribute, valueAttribute} = options

      return normalizeItems({data, labelAttribute, records, valueAttribute})
    })
    .catch((err) => {
      Logger.error(`Error fetching endpoint "${options.endpoint}"`, err)
      throw err
    })
}

/**
 * Fetch the list of network functions from the backend and set them
 * @param {Object} value the bunsen value for this form
 * @param {Object} modelDef the full bunsen model def for this property
 * @param {Object[]} data initializes the list with this
 * @param {Object} bunsenId the bunsenId for this form
 * @param {Object} store the ember-data store
 * @param {String} filter the partial match query filter to populate
 * @param {Boolean} keepCurrentValue determines whether we need to refetch for the current value
 * @returns {RSVP.Promise} a promise that resolves to the list of items
 */
export function getItemsFromEmberData ({value, modelDef, data, bunsenId, store, filter, keepCurrentValue = true}) {
  const modelType = modelDef.modelType || 'resources'
  const {labelAttribute, queryForCurrentValue, valueAttribute} = modelDef
  const valueAsId = get(value, bunsenId)
  let arrayValues
  if (isArray(valueAsId) && queryForCurrentValue) {
    arrayValues = valueAsId.asMutable()  // frost-multi-select does not support immutable values
    arrayValues = RSVP.Promise.all(arrayValues.map((id) => store.findRecord(modelType, id)))
      .catch((err) => {
        Logger.log(`Error fetching ${modelType}`, err)
        throw err
      })
  }
  const actuallyFindCurrentValue = keepCurrentValue && queryForCurrentValue && valueAsId !== undefined && !arrayValues

  const query = getQuery({
    bunsenId,
    filter,
    query: modelDef.query,
    value
  })

  return RSVP.hash({
    items: store.query(modelType, query)
      .then((records) => {
        return normalizeItems({data, labelAttribute, records, valueAttribute})
      })
      .catch((err) => {
        Logger.log(`Error fetching ${modelType}`, err)
        throw err
      }),
    valueRecord: actuallyFindCurrentValue ? store.findRecord(modelType, valueAsId)
      .catch((err) => {
        Logger.log(`Error fetching ${modelType}`, err)
        throw err
      }) : RSVP.resolve(null),
    arrayRecords: arrayValues || RSVP.resolve(null)
  })
    .then(({arrayRecords, items, valueRecord}) => {
      if (actuallyFindCurrentValue &&
        shouldAddCurrentValue({items, valueRecord, labelAttribute, valueAttribute, filter})) {
        return normalizeItems({data: items, labelAttribute, records: [valueRecord], valueAttribute})
      }

      if (arrayRecords) {
        const recordsToAdd = arrayRecords.filter(record => {
          return shouldAddCurrentValue({items, valueRecord: record, labelAttribute, valueAttribute, filter})
        })
        return normalizeItems({data: items, labelAttribute, records: recordsToAdd, valueAttribute})
      }
      return items
    })
}

/**
 * Fetch the list of items from elsewhere in the form value
 * @param {Object[]} data - initializes the list with this
 * @param {String} filter - the partial match query filter to populate
 * @param {Object} options - bunsen model for this property plus view renderer options
 * @param {Object} value - the bunsen value for this form
 * @returns {RSVP.Promise} a promise that resolves with the list of items
 */
export function getItemsFromFormValue ({bunsenId, data, filter, options, value}) {
  const {recordsPath} = options

  let records
  if (recordsPath[0] === '.') {
    records = new ValueWrapper(value, bunsenId).get(recordsPath)
  } else {
    records = get(value, recordsPath)
  }

  if (!isArray(records)) {
    Logger.warn(
      `Expected an array of records at "${recordsPath}" but got:`,
      records
    )

    return RSVP.resolve([])
  }

  const {labelAttribute, valueAttribute} = options

  return RSVP.resolve(
    normalizeItems({data, labelAttribute, records, valueAttribute})
  )
}

/**
 * Converts records from an API response into a standard format with "label"
 * and "value" properties so renderers can predictably process the data.
 * @param {Object[]} data - initializes the list with this
 * @param {String} labelAttribute - dot notated path to label attribute in record
 * @param {Array<Object>} records - records to normalize
 * @param {String} valueAttribute - dot notated path to value attribute in record
 * @returns {Array<Object>} normalized items
 */
function normalizeItems ({data, labelAttribute, records, valueAttribute}) {
  const labelAttr = labelAttribute || 'label'
  const valueAttr = valueAttribute || 'id'

  return data.concat(
    records.map((record) => {
      if (typeof record !== 'object') {
        return {
          label: `${record}`, // make sure label is a string
          value: record
        }
      }

      let label, value

      if (labelAttr.indexOf('${') !== -1) {
        label = utils.parseVariables(record, labelAttr)
      } else {
        label = get(record, labelAttr) || get(record, 'title')
      }

      if (valueAttr.indexOf('${') !== -1) {
        value = utils.parseVariables(record, valueAttr)
      } else {
        value = get(record, valueAttr)
      }

      return {
        label,
        value
      }
    })
  )
}

/**
 * Determine whether or not valueRecord should be appended to data
 * @param {Object[]} items - the larger set of data
 * @param {EmberObject} valueRecord - the record to add or not
 * @param {String} labelAttribute - dot notated path to label attribute in record
 * @param {String} valueAttribute - dot notated path to value attribute in record
 * @param {String} filter - the partial match query filter to populate
 * @returns {boolean} true if valueRecord should be added to items
 */
function shouldAddCurrentValue ({items, valueRecord, labelAttribute, valueAttribute, filter}) {
  const filterRegex = new RegExp(filter, 'i')
  const valueRecordMatchesFilter = filterRegex.test(valueRecord.get(labelAttribute))
  const itemsContainsValueRecord = items.find(item => item.value === valueRecord.get(valueAttribute))
  return valueRecordMatchesFilter && !itemsContainsValueRecord
}
<commit_msg>Fixed linting<commit_after>/**
 * Utilitiy functions for working with lists, mainly used by select renderer
 */

import {utils} from 'bunsen-core'
const {ValueWrapper} = utils.path
import Ember from 'ember'
const {Logger, RSVP, get, typeOf} = Ember

const {isArray} = Array
const {keys} = Object

/**
 * set a list's available options
 * @param {String} bunsenId - the bunsen id for this property
 * @param {Object[]} data - initializes the list with this
 * @param {String} filter - the optional string to filter on
 * @param {Object} options - the bunsen model definition
 * @param {Object} store - the ember-data store
 * @param {Object} value - the current value object for the form instance
 * @param {Boolean} keepCurrentValue - determines whether we need to refetch for the current value
 * @returns {RSVP.Promise} a promise that resolves to a list of items
 */
export function getOptions ({ajax, bunsenId, data, filter = '', options, store, value, keepCurrentValue}) {
  // escape regexp metacharacters for local data filtering since filter is not a regular expression
  const safeFilter = filter.replace(/[.?*+^$[\]\\(){}|-]/g, '\\$&')
  const filterRegex = new RegExp(safeFilter, 'i')
  const filteredData = data.filter((item) => filterRegex.test(item.label))

  if (options.modelType) {
    return getItemsFromEmberData(
      {value, modelDef: options, data: filteredData, bunsenId, store, filter, keepCurrentValue})
  } else if (options.endpoint) {
    return getItemsFromAjaxCall({ajax, bunsenId, data: filteredData, filter, options, value})
  } else if (options.recordsPath) {
    return getItemsFromFormValue({bunsenId, data: filteredData, filter, options, value})
  }

  return RSVP.resolve(filteredData)
}

/**
 * Get query with property references replaced by property values.
 * @param {String} bunsenId - bunsen ID of property (used for relative references)
 * @param {String} filter - the partial match query filter to populate
 * @param {Object} query - query to process references in
 * @param {Object} value - the bunsen value for this form
 * @returns {Object} query object with references filled in
 */
export function getQuery ({bunsenId, filter, query, value}) {
  const result = utils.populateQuery(value, query, bunsenId)

  if (typeOf(result) !== 'object') {
    return result
  }

  // replace the special $filter placeholder with the filter value (if it exists)
  keys(result).forEach((key) => {
    const value = result[key]

    if (typeOf(value) === 'string') {
      result[key] = value.replace('$filter', filter)
    }
  })

  return result
}

/**
 * take enum values and make into a collection
 * @param  {String[]} values the values to make into a collection
 * @param {String} filter - filter
 * @returns {Object[]} a list of item objects
 */
export function getEnumValues (values = [], filter = '') {
  const filteredValues = []
  values.filter((value) => {
    if (!filter || value.toLowerCase().indexOf(filter.toLowerCase()) !== -1) {
      filteredValues.push({label: value.toString(), value})
    }
  })
  return filteredValues
}

/**
 * Fetch the list of items from an API endpoint
 * @param {Object} ajax - ember-ajax service
 * @param {Object} bunsenId - the bunsenId for this form
 * @param {Object[]} data - initializes the list with this
 * @param {String} filter - the partial match query filter to populate
 * @param {Object} options - bunsen model for this property plus view renderer options
 * @param {Object} value - the bunsen value for this form
 * @returns {RSVP.Promise} a promise that resolves with the list of items
 */
export function getItemsFromAjaxCall ({ajax, bunsenId, data, filter, options, value}) {
  const query = getQuery({
    bunsenId,
    filter,
    query: options.query,
    value
  })

  const queryString = keys(query)
    .map((key) => `${key}=${query[key]}`)
    .join('&')

  const url = queryString ? `${options.endpoint}?${queryString}` : options.endpoint

  return ajax.request(url)
    .then((result) => {
      const records = options.recordsPath ? get(result, options.recordsPath) : result

      if (!isArray(records)) {
        Logger.warn(
          `Expected an array of records at "${options.recordPath}" but got:`,
          records
        )

        return []
      }

      const {labelAttribute, valueAttribute} = options

      return normalizeItems({data, labelAttribute, records, valueAttribute})
    })
    .catch((err) => {
      Logger.error(`Error fetching endpoint "${options.endpoint}"`, err)
      throw err
    })
}

/**
 * Fetch the list of network functions from the backend and set them
 * @param {Object} value the bunsen value for this form
 * @param {Object} modelDef the full bunsen model def for this property
 * @param {Object[]} data initializes the list with this
 * @param {Object} bunsenId the bunsenId for this form
 * @param {Object} store the ember-data store
 * @param {String} filter the partial match query filter to populate
 * @param {Boolean} keepCurrentValue determines whether we need to refetch for the current value
 * @returns {RSVP.Promise} a promise that resolves to the list of items
 */
export function getItemsFromEmberData ({value, modelDef, data, bunsenId, store, filter, keepCurrentValue = true}) {
  const modelType = modelDef.modelType || 'resources'
  const {labelAttribute, queryForCurrentValue, valueAttribute} = modelDef
  const valueAsId = get(value, bunsenId)
  let arrayValues
  if (isArray(valueAsId) && queryForCurrentValue) {
    arrayValues = valueAsId.asMutable() // frost-multi-select does not support immutable values
    arrayValues = RSVP.Promise.all(arrayValues.map((id) => store.findRecord(modelType, id)))
      .catch((err) => {
        Logger.log(`Error fetching ${modelType}`, err)
        throw err
      })
  }
  const actuallyFindCurrentValue = keepCurrentValue && queryForCurrentValue && valueAsId !== undefined && !arrayValues

  const query = getQuery({
    bunsenId,
    filter,
    query: modelDef.query,
    value
  })

  return RSVP.hash({
    items: store.query(modelType, query)
      .then((records) => {
        return normalizeItems({data, labelAttribute, records, valueAttribute})
      })
      .catch((err) => {
        Logger.log(`Error fetching ${modelType}`, err)
        throw err
      }),
    valueRecord: actuallyFindCurrentValue ? store.findRecord(modelType, valueAsId)
      .catch((err) => {
        Logger.log(`Error fetching ${modelType}`, err)
        throw err
      }) : RSVP.resolve(null),
    arrayRecords: arrayValues || RSVP.resolve(null)
  })
    .then(({arrayRecords, items, valueRecord}) => {
      if (actuallyFindCurrentValue &&
        shouldAddCurrentValue({items, valueRecord, labelAttribute, valueAttribute, filter})) {
        return normalizeItems({data: items, labelAttribute, records: [valueRecord], valueAttribute})
      }

      if (arrayRecords) {
        const recordsToAdd = arrayRecords.filter(record => {
          return shouldAddCurrentValue({items, valueRecord: record, labelAttribute, valueAttribute, filter})
        })
        return normalizeItems({data: items, labelAttribute, records: recordsToAdd, valueAttribute})
      }
      return items
    })
}

/**
 * Fetch the list of items from elsewhere in the form value
 * @param {Object[]} data - initializes the list with this
 * @param {String} filter - the partial match query filter to populate
 * @param {Object} options - bunsen model for this property plus view renderer options
 * @param {Object} value - the bunsen value for this form
 * @returns {RSVP.Promise} a promise that resolves with the list of items
 */
export function getItemsFromFormValue ({bunsenId, data, filter, options, value}) {
  const {recordsPath} = options

  let records
  if (recordsPath[0] === '.') {
    records = new ValueWrapper(value, bunsenId).get(recordsPath)
  } else {
    records = get(value, recordsPath)
  }

  if (!isArray(records)) {
    Logger.warn(
      `Expected an array of records at "${recordsPath}" but got:`,
      records
    )

    return RSVP.resolve([])
  }

  const {labelAttribute, valueAttribute} = options

  return RSVP.resolve(
    normalizeItems({data, labelAttribute, records, valueAttribute})
  )
}

/**
 * Converts records from an API response into a standard format with "label"
 * and "value" properties so renderers can predictably process the data.
 * @param {Object[]} data - initializes the list with this
 * @param {String} labelAttribute - dot notated path to label attribute in record
 * @param {Array<Object>} records - records to normalize
 * @param {String} valueAttribute - dot notated path to value attribute in record
 * @returns {Array<Object>} normalized items
 */
function normalizeItems ({data, labelAttribute, records, valueAttribute}) {
  const labelAttr = labelAttribute || 'label'
  const valueAttr = valueAttribute || 'id'

  return data.concat(
    records.map((record) => {
      if (typeof record !== 'object') {
        return {
          label: `${record}`, // make sure label is a string
          value: record
        }
      }

      let label, value

      if (labelAttr.indexOf('${') !== -1) {
        label = utils.parseVariables(record, labelAttr)
      } else {
        label = get(record, labelAttr) || get(record, 'title')
      }

      if (valueAttr.indexOf('${') !== -1) {
        value = utils.parseVariables(record, valueAttr)
      } else {
        value = get(record, valueAttr)
      }

      return {
        label,
        value
      }
    })
  )
}

/**
 * Determine whether or not valueRecord should be appended to data
 * @param {Object[]} items - the larger set of data
 * @param {EmberObject} valueRecord - the record to add or not
 * @param {String} labelAttribute - dot notated path to label attribute in record
 * @param {String} valueAttribute - dot notated path to value attribute in record
 * @param {String} filter - the partial match query filter to populate
 * @returns {boolean} true if valueRecord should be added to items
 */
function shouldAddCurrentValue ({items, valueRecord, labelAttribute, valueAttribute, filter}) {
  const filterRegex = new RegExp(filter, 'i')
  const valueRecordMatchesFilter = filterRegex.test(valueRecord.get(labelAttribute))
  const itemsContainsValueRecord = items.find(item => item.value === valueRecord.get(valueAttribute))
  return valueRecordMatchesFilter && !itemsContainsValueRecord
}
<|endoftext|>


<commit_before>"use strict";

/*global nodeca, _*/

var UserGroup = nodeca.models.users.UserGroup;

// Validate input parameters
//
var params_schema = {
  _id: {
    type: 'string',
    required: true
  }
};
nodeca.validate(params_schema);

/**
 * admin.usergroups.show(params, callback) -> Void
 *
 *
 * Display usergrroup
 *
 **/
module.exports = function (params, next) {
  var env = this;

  UserGroup.findOne({_id: params._id}).setOptions({ lean: true }).exec(function(err, group) {
    if (err) {
      next(err);
      return;
    }
    if (!group) {
      next({ statusCode: nodeca.io.NOT_FOUND });
      return;
    }

    var settings = nodeca.config.setting_schemas['usergroup'];

    // collect  usergroups items and group it by setting group
    var item_groups = {};
    _.keys(settings).forEach(function(name) {
      var item = settings[name];
      // get value from model if exists
      if (name in group.items) {
        item.value = group.items[name];
      }

      var group_name = item['group'];
      if (!item_groups[group_name]) {
        item_groups[group_name] = {};
      }
      item_groups[group_name][name] = item;
    });
    env.data.item_groups = item_groups;

    env.data.usergroup = group;
    next();
  });
};

// Put usergroups items into response data
//
nodeca.filters.after('@', function (params, next) {
  this.response.data.usergroup_id = this.data.usergroup._id;
  this.response.data.item_groups = this.data.item_groups;

  next();
});
<commit_msg>Fixed showing usergroup query<commit_after>"use strict";

/*global nodeca, _*/

var UserGroup = nodeca.models.users.UserGroup;

// Validate input parameters
//
var params_schema = {
  _id: {
    type: 'string',
    required: true
  }
};
nodeca.validate(params_schema);

/**
 * admin.usergroups.show(params, callback) -> Void
 *
 *
 * Display usergrroup
 *
 **/
module.exports = function (params, next) {
  var env = this;

  UserGroup.findOne({_id: params._id}).exec(function(err, group) {
    if (err) {
      next(err);
      return;
    }
    if (!group) {
      next({ statusCode: nodeca.io.NOT_FOUND });
      return;
    }

    var settings = nodeca.config.setting_schemas['usergroup'];

    // collect  usergroups items and group it by setting group
    var item_groups = {};
    _.keys(settings).forEach(function(name) {
      var item = settings[name];
      // get value from model if exists
      if (name in group.items) {
        item.value = group.items[name];
      }

      var group_name = item['group'];
      if (!item_groups[group_name]) {
        item_groups[group_name] = {};
      }
      item_groups[group_name][name] = item;
    });
    env.data.item_groups = item_groups;

    env.data.usergroup = group;
    next();
  });
};

// Put usergroups items into response data
//
nodeca.filters.after('@', function (params, next) {
  this.response.data.usergroup_id = this.data.usergroup._id;
  this.response.data.item_groups = this.data.item_groups;

  next();
});
<|endoftext|>