<commit_before>"use strict";

Template.eventReplication.onCreated(function() {
	this.replicaDates = new ReactiveVar([]);
});

Template.eventReplication.onRendered(function() {
	this.$('.js-replicate-date').datepicker({
		weekStart: moment.localeData().firstDayOfWeek(),
		language: moment.locale(),
		autoclose: true,
		startDate: new Date(),
		format: {
			toDisplay: function(date) {
				return moment(date).format('L');
			},
			toValue: function(date) {
				return moment(date, 'L').toDate();
			}
		}
	});

	updateReplicas(this);
});

var replicaStartDate = function(originalDate) {
	var originalMoment = moment(originalDate);
	var startMoment = moment.max(originalMoment, moment());
	startMoment.day(originalMoment.day());
	return startMoment;
};

Template.eventReplication.helpers({
	replicaStart: function() {
		return replicaStartDate(this.start).format("L");
	},

	replicaEnd: function() {
		return replicaStartDate(this.start).add(1, 'week').format("L");
	},

	localDate: function(date) {
		return moment(date).format("D. MMM");
	},

	affectedReplicaCount: function() {
		Template.instance().subscribe('affectedReplica', this._id);
		return Events.find(affectedReplicaSelectors(this)).count();
	},

	replicaDateCount: function() {
		return Template.instance().replicaDates.get().length;
	},

	replicaDates: function() {
		return Template.instance().replicaDates.get();
	},
});

var updateReplicas = function(template) {
	template.replicaDates.set(_.map(getEventFrequency(template), function(interval) { return interval[0]; } ));
};

var getEventFrequency = function(template) {
	var startDate = moment(template.$('#replicateStart').val(), 'L');
	if (!startDate.isValid()) return [];
	if (startDate.isBefore(moment())) {
		// Jump forward in time so we don't have to look at all these old dates
		startDate = replicaStartDate(startDate);
	}

	var endDate   = moment(template.$('#replicateEnd').val(), 'L');
	if (!endDate.isValid()) return [];
	var frequency = template.$('.js-replicate-frequency:checked').val();

	var frequencies = { once:          { unit: 'days',
	                                     interval: 1 },
	                    daily:         { unit: 'days',
	                                     interval: 1 },
	                    weekly:        { unit: 'weeks',
	                                     interval: 1 },
	                    biWeekly:      { unit: 'weeks',
	                                     interval: 2 }};

	var unit = frequencies[frequency].unit;
	if (unit === undefined) return [];

	var interval = frequencies[frequency].interval;

	var eventStart = moment(template.data.start);
	var originDay = moment(eventStart).startOf('day');
	var eventEnd = moment(template.data.end);

	var now = moment();
	var repStart = moment(startDate).startOf('day');
	var dates = [];
	while(!repStart.isAfter(endDate)) {
		var daysFromOriginal = repStart.diff(originDay, 'days');
		if (daysFromOriginal !== 0 && repStart.isAfter(now)) {
			dates.push([
				moment(eventStart).add(daysFromOriginal, 'days'),
				moment(eventEnd).add(daysFromOriginal, 'days')
			]);
			if (frequency == 'once') break;
			if (dates.length >= 52) break;
		}

		repStart.add(interval, unit);
	}

	return dates;
};

Template.eventReplication.events({
	'click .js-replicate-btn': function (event, template) {
		//get all startDates where the event should be created
		//this does not do anything yet other than generating the start-end times for a given period

		var dates = getEventFrequency(template);
		var success = true;
		$.each( dates, function( i,eventTime ) {

			/*create a new event for each time interval */
			var replicaEvent = {
				title: template.data.title,
				description: template.data.description,
				location: template.data.location,
				room: template.data.room || '',
				start: eventTime[0].toDate(),
				end: eventTime[1].toDate(),
				files: template.data.files  || [],
				region: template.data.region || Session.get('region'),
				groups: template.data.groups,
				replicaOf: template.data.replicaOf || template.data._id, // delegate the same replicaOf ID for this replica if the replicated event is also a replica
			};

			var courseId = template.data.courseId;
			if (courseId) {
				replicaEvent.courseId = courseId;
			}

			var eventId = '';

			Meteor.call('saveEvent', eventId, replicaEvent, function(error, eventId) {
				if (error) {
					showServerError('Replicating the event went wrong', error);
					success = false;
				} else {
					var fmtDate = moment(replicaEvent.start).format('LL');
					addMessage(mf('event.replicate.success', { TITLE: template.data.title, DATE: fmtDate }, 'Cloned event "{TITLE}" for {DATE}'), 'success');
				}
			});
		});

		template.parentInstance().replicating.set(false);
	},

	'mouseover .js-replicate-btn': function(event, instance) {
		instance.$('.replica-event-captions').addClass('highlighted');
	},

	'mouseout .js-replicate-btn': function(event, instance) {
		instance.$('.replica-event-captions').removeClass('highlighted');
	},

	'change .js-update-replicas, keyup .js-update-replicas': function(event, template) {
		updateReplicas(template);
	}
});
<commit_msg>small formatting change<commit_after>"use strict";

Template.eventReplication.onCreated(function() {
	this.replicaDates = new ReactiveVar([]);
});

Template.eventReplication.onRendered(function() {
	this.$('.js-replicate-date').datepicker({
		weekStart: moment.localeData().firstDayOfWeek(),
		language: moment.locale(),
		autoclose: true,
		startDate: new Date(),
		format: {
			toDisplay: function(date) {
				return moment(date).format('L');
			},
			toValue: function(date) {
				return moment(date, 'L').toDate();
			}
		}
	});

	updateReplicas(this);
});

var replicaStartDate = function(originalDate) {
	var originalMoment = moment(originalDate);
	var startMoment = moment.max(originalMoment, moment());
	startMoment.day(originalMoment.day());
	return startMoment;
};

Template.eventReplication.helpers({
	replicaStart: function() {
		return replicaStartDate(this.start).format("L");
	},

	replicaEnd: function() {
		return replicaStartDate(this.start).add(1, 'week').format("L");
	},

	localDate: function(date) {
		return moment(date).format("D. MMM");
	},

	affectedReplicaCount: function() {
		Template.instance().subscribe('affectedReplica', this._id);
		return Events.find(affectedReplicaSelectors(this)).count();
	},

	replicaDateCount: function() {
		return Template.instance().replicaDates.get().length;
	},

	replicaDates: function() {
		return Template.instance().replicaDates.get();
	},
});

var updateReplicas = function(template) {
	template.replicaDates.set(_.map(getEventFrequency(template), function(interval) { return interval[0]; } ));
};

var getEventFrequency = function(template) {
	var startDate = moment(template.$('#replicateStart').val(), 'L');
	if (!startDate.isValid()) return [];
	if (startDate.isBefore(moment())) {
		// Jump forward in time so we don't have to look at all these old dates
		startDate = replicaStartDate(startDate);
	}

	var endDate   = moment(template.$('#replicateEnd').val(), 'L');
	if (!endDate.isValid()) return [];
	var frequency = template.$('.js-replicate-frequency:checked').val();

	var frequencies = { once:     { unit: 'days',  interval: 1 },
	                    daily:    { unit: 'days',  interval: 1 },
	                    weekly:   { unit: 'weeks', interval: 1 },
	                    biWeekly: { unit: 'weeks', interval: 2 } };

	var unit = frequencies[frequency].unit;
	if (unit === undefined) return [];

	var interval = frequencies[frequency].interval;

	var eventStart = moment(template.data.start);
	var originDay = moment(eventStart).startOf('day');
	var eventEnd = moment(template.data.end);

	var now = moment();
	var repStart = moment(startDate).startOf('day');
	var dates = [];
	while(!repStart.isAfter(endDate)) {
		var daysFromOriginal = repStart.diff(originDay, 'days');
		if (daysFromOriginal !== 0 && repStart.isAfter(now)) {
			dates.push([
				moment(eventStart).add(daysFromOriginal, 'days'),
				moment(eventEnd).add(daysFromOriginal, 'days')
			]);
			if (frequency == 'once') break;
			if (dates.length >= 52) break;
		}

		repStart.add(interval, unit);
	}

	return dates;
};

Template.eventReplication.events({
	'click .js-replicate-btn': function (event, template) {
		//get all startDates where the event should be created
		//this does not do anything yet other than generating the start-end times for a given period

		var dates = getEventFrequency(template);
		var success = true;
		$.each( dates, function( i,eventTime ) {

			/*create a new event for each time interval */
			var replicaEvent = {
				title: template.data.title,
				description: template.data.description,
				location: template.data.location,
				room: template.data.room || '',
				start: eventTime[0].toDate(),
				end: eventTime[1].toDate(),
				files: template.data.files  || [],
				region: template.data.region || Session.get('region'),
				groups: template.data.groups,
				replicaOf: template.data.replicaOf || template.data._id, // delegate the same replicaOf ID for this replica if the replicated event is also a replica
			};

			var courseId = template.data.courseId;
			if (courseId) {
				replicaEvent.courseId = courseId;
			}

			var eventId = '';

			Meteor.call('saveEvent', eventId, replicaEvent, function(error, eventId) {
				if (error) {
					showServerError('Replicating the event went wrong', error);
					success = false;
				} else {
					var fmtDate = moment(replicaEvent.start).format('LL');
					addMessage(mf('event.replicate.success', { TITLE: template.data.title, DATE: fmtDate }, 'Cloned event "{TITLE}" for {DATE}'), 'success');
				}
			});
		});

		template.parentInstance().replicating.set(false);
	},

	'mouseover .js-replicate-btn': function(event, instance) {
		instance.$('.replica-event-captions').addClass('highlighted');
	},

	'mouseout .js-replicate-btn': function(event, instance) {
		instance.$('.replica-event-captions').removeClass('highlighted');
	},

	'change .js-update-replicas, keyup .js-update-replicas': function(event, template) {
		updateReplicas(template);
	}
});
<|endoftext|>


<commit_before>import randId from '../randId';

const defaultPrefix = 'node-';
const fooPrefix = 'foo-';

it('generates a random ID, prefixed by "node-" by default', () => {
    const result = randId();
    expect(result).toEqual(expect.stringMatching(/^node-/));
});

it('generates a random ID at length of 10 chars (+5 for prefix) by default', () => {
    const result = randId();
    expect(result).toHaveLength(defaultPrefix.length + 10);
});

it('can change output prefix', () => {
    const result = randId({ prefix: fooPrefix });
    expect(result).toEqual(expect.stringMatching(/^foo-/));
});

it('can change output length', () => {
    const result = randId({ length: 6 });
    expect(result).toHaveLength(defaultPrefix.length + 6);
});

it('can change both output prefix and length', () => {
    const result = randId({ prefix: fooPrefix, length: 12 });
    expect(result).toEqual(expect.stringMatching(/^foo-/));
    expect(result).toHaveLength(fooPrefix.length + 12 + 1);
});
<commit_msg>Add comment for randId testing.<commit_after>import randId from '../randId';

const defaultPrefix = 'node-';
const fooPrefix = 'foo-';

it('generates a random ID, prefixed by "node-" by default', () => {
    const result = randId();
    expect(result).toEqual(expect.stringMatching(/^node-/));
});

it('generates a random ID at length of 10 chars (+5 for prefix) by default', () => {
    const result = randId();
    expect(result).toHaveLength(defaultPrefix.length + 10);
});

it('can change output prefix', () => {
    const result = randId({ prefix: fooPrefix });
    expect(result).toEqual(expect.stringMatching(/^foo-/));
});

it('can change output length', () => {
    const result = randId({ length: 6 });
    expect(result).toHaveLength(defaultPrefix.length + 6);
});

it('can change both output prefix and length', () => {
    const result = randId({ prefix: fooPrefix, length: 12 });
    expect(result).toEqual(expect.stringMatching(/^foo-/));
    // plus 1 for default middle char '-'
    expect(result).toHaveLength(fooPrefix.length + 12 + 1);
});
<|endoftext|>


<commit_before>var s3 = require('s3'),
	path = require('path'),
	fs = require('fs'),
	os = require('os'),
	assert = require('assert'),
	swintSecret = require('../lib');

global.swintVar.printLevel = 5;

describe('secret', function() {
	this.timeout(10000);

	before(function(done) {
		var credPath = path.join(process.env.HOME, '.swint', 'swint-secret-test.json'),
			cred = JSON.parse(fs.readFileSync(credPath)),
			client = s3.createClient({
				s3Options: {
					accessKeyId: cred.id,
					secretAccessKey: cred.secret
				}
			}),
			params = {
				localDir: path.join(__dirname, '../test_case'),
				deleteRemoved: true,
				s3Params: {
					Bucket: cred.bucket,
					Prefix: ''
				}
			};

		fs.mkdirSync(path.join(os.tmpDir(), 'swint-secret-empty'));

		var uploader = client.uploadDir(params);

		uploader.on('error', function(err) {
			print(4, err);
			process.exit(-1);
		});

		uploader.on('end', function() {
			done();
		});
	});

	it('_getPaths()', function() {
		var ss = new swintSecret({
			paths: {
				a: 'aaa',
				b: 'bbb',
				c: {
					d: ['ddd1', 'ddd2', 'ddd3'],
					e: 'eee'
				}
			}
		});

		assert.deepEqual(ss._getPaths(ss.options.paths), ['aaa', 'bbb', 'ddd1', 'ddd2', 'ddd3', 'eee']);
	});

	it('_setPaths()', function() {
		var ss = new swintSecret({
			paths: {
				a: 'aaa',
				b: 'bbb',
				c: {
					d: ['ddd1', 'ddd2', 'ddd3'],
					e: 'eee'
				}
			}
		});

		ss._setPaths(ss.options.paths, {
			aaa: 'AAA',
			bbb: 'BBB',
			ddd1: 'DDD1',
			ddd2: 'DDD2',
			ddd3: 'DDD3',
			eee: 'EEE'
		});

		assert.deepEqual(ss.options.paths, {
			a: 'AAA',
			b: 'BBB',
			c: {
				d: ['DDD1', 'DDD2', 'DDD3'],
				e: 'EEE'
			}
		});
	});

	it('Simple case', function(done) {
		var ss = new swintSecret({
			bucket: 'swint-secret',
			paths: {
				a: 'aaa',
				b: 'bbb',
				c: {
					d: ['ddd1', 'ddd2', 'ddd3'],
					e: 'eee'
				}
			}
		});

		ss.ready(function(err, res) {
			if(err) {
				print(4, err);
				process.exit(-1);
			}

			assert.deepEqual(res, {
				a: 'AAA',
				b: 'BBB',
				c: {
					d: ['DDD1', 'DDD2', 'DDD3'],
					e: 'EEE'
				}
			});

			done();
		});
	});

	after(function(done) {
		var credPath = path.join(process.env.HOME, '.swint', 'swint-secret-test.json'),
			cred = JSON.parse(fs.readFileSync(credPath)),
			client = s3.createClient({
				s3Options: {
					accessKeyId: cred.id,
					secretAccessKey: cred.secret
				}
			}),
			params = {
				localDir: path.join(os.tmpDir(), 'swint-secret-empty'),
				deleteRemoved: true,
				s3Params: {
					Bucket: 'swint-secret',
					Prefix: ''
				}
			};

		var uploader = client.uploadDir(params);

		uploader.on('error', function(err) {
			print(4, err);
			process.exit(-1);
		});

		uploader.on('end', function() {
			fs.rmdirSync(path.join(os.tmpDir(), 'swint-secret-empty'));
			done();
		});
	});
});
<commit_msg>Added env variable support on test<commit_after>var s3 = require('s3'),
	path = require('path'),
	fs = require('fs'),
	os = require('os'),
	assert = require('assert'),
	swintSecret = require('../lib');

global.swintVar.printLevel = 5;

describe('secret', function() {
	this.timeout(10000);

	before(function(done) {
		var credPath = path.join(process.env.HOME, '.swint', 'swint-secret-test.json'),
			cred;

		try {
			fs.accessSync(credPath);
			cred = JSON.parse(fs.readFileSync(credPath));
		} catch(e) {
			cred = {
				id: process.env.SWINT_SECRET_TEST_ID,
				secret: process.env.SWINT_SECRET_TEST_SECRET,
				bucket: process.env.SWINT_SECRET_TEST_BUCKET
			};
		}

		var client = s3.createClient({
				s3Options: {
					accessKeyId: cred.id,
					secretAccessKey: cred.secret
				}
			}),
			params = {
				localDir: path.join(__dirname, '../test_case'),
				deleteRemoved: true,
				s3Params: {
					Bucket: cred.bucket,
					Prefix: ''
				}
			};

		fs.mkdirSync(path.join(os.tmpDir(), 'swint-secret-empty'));

		var uploader = client.uploadDir(params);

		uploader.on('error', function(err) {
			print(4, err);
			process.exit(-1);
		});

		uploader.on('end', function() {
			done();
		});
	});

	it('_getPaths()', function() {
		var ss = new swintSecret({
			paths: {
				a: 'aaa',
				b: 'bbb',
				c: {
					d: ['ddd1', 'ddd2', 'ddd3'],
					e: 'eee'
				}
			}
		});

		assert.deepEqual(ss._getPaths(ss.options.paths), ['aaa', 'bbb', 'ddd1', 'ddd2', 'ddd3', 'eee']);
	});

	it('_setPaths()', function() {
		var ss = new swintSecret({
			paths: {
				a: 'aaa',
				b: 'bbb',
				c: {
					d: ['ddd1', 'ddd2', 'ddd3'],
					e: 'eee'
				}
			}
		});

		ss._setPaths(ss.options.paths, {
			aaa: 'AAA',
			bbb: 'BBB',
			ddd1: 'DDD1',
			ddd2: 'DDD2',
			ddd3: 'DDD3',
			eee: 'EEE'
		});

		assert.deepEqual(ss.options.paths, {
			a: 'AAA',
			b: 'BBB',
			c: {
				d: ['DDD1', 'DDD2', 'DDD3'],
				e: 'EEE'
			}
		});
	});

	it('Simple case', function(done) {
		var ss = new swintSecret({
			bucket: 'swint-secret',
			paths: {
				a: 'aaa',
				b: 'bbb',
				c: {
					d: ['ddd1', 'ddd2', 'ddd3'],
					e: 'eee'
				}
			}
		});

		ss.ready(function(err, res) {
			if(err) {
				print(4, err);
				process.exit(-1);
			}

			assert.deepEqual(res, {
				a: 'AAA',
				b: 'BBB',
				c: {
					d: ['DDD1', 'DDD2', 'DDD3'],
					e: 'EEE'
				}
			});

			done();
		});
	});

	after(function(done) {
		var credPath = path.join(process.env.HOME, '.swint', 'swint-secret-test.json'),
			cred;

		try {
			fs.accessSync(credPath);
			cred = JSON.parse(fs.readFileSync(credPath));
		} catch(e) {
			cred = {
				id: process.env.SWINT_SECRET_TEST_ID,
				secret: process.env.SWINT_SECRET_TEST_SECRET,
				bucket: process.env.SWINT_SECRET_TEST_BUCKET
			};
		}

		var client = s3.createClient({
				s3Options: {
					accessKeyId: cred.id,
					secretAccessKey: cred.secret
				}
			}),
			params = {
				localDir: path.join(os.tmpDir(), 'swint-secret-empty'),
				deleteRemoved: true,
				s3Params: {
					Bucket: 'swint-secret',
					Prefix: ''
				}
			};

		var uploader = client.uploadDir(params);

		uploader.on('error', function(err) {
			print(4, err);
			process.exit(-1);
		});

		uploader.on('end', function() {
			fs.rmdirSync(path.join(os.tmpDir(), 'swint-secret-empty'));
			done();
		});
	});
});
<|endoftext|>


<commit_before><commit_msg>broke up submit div function<commit_after><|endoftext|>


<commit_before>// Copyright 2018, University of Colorado Boulder

/**
 * An immutable quadratic, described by the equation y = ax^2 + bx + c.
 *
 * Typically, a quadratic requires a !== 0. But this sim is required to support a === 0.
 * So there is some non-standard behavior herein that is not exactly mathematically correct.
 * Specifically:
 *
 * (1) When a === 0, Quadratic behaves like a straight line.  See for example solveX.
 *
 * (2) When a === 0, y = c (standard form) and y = k (vertex form) describe the same horizontal line.
 *     See for example createFromVertexForm.
 *
 * @author Andrea Lin
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( require => {
  'use strict';

  // modules
  const graphingQuadratics = require( 'GRAPHING_QUADRATICS/graphingQuadratics' );
  const GQColors = require( 'GRAPHING_QUADRATICS/common/GQColors' );
  const StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  const Util = require( 'DOT/Util' );
  const Vector2 = require( 'DOT/Vector2' );

  // constants
  const EPSILON = 1e-8;  // how close is considered to be "on" the quadratic

  class Quadratic {

    /**
     * @param {number} a - coefficient for the quadratic term
     * @param {number} b - coefficient for the linear term
     * @param {number} c - constant term
     * @param {Object} [options]
     */
    constructor( a, b, c, options ) {

      options = _.extend( {
        color: 'black' // {Color|String} color used to render the curve
      }, options );

      // @public (read-only)
      this.a = a;
      this.b = b;
      this.c = c;
      this.color = options.color;

      // @public (read-only) {Vector2[]|null} null means that all points are roots (y = 0)
      this.roots = solveRoots( a, b, c );

      // Strictly speaking, we don't have a quadratic if a === 0.
      // If that's the case, then the fields in this if block will be undefined.
      if ( a !== 0 ) {

        // @public (read-only) derive coefficients for vertex form, y = (1/(4p))(x - h)^2 + k
        this.p = 1 / ( 4 * a );
        this.h = -b / ( 2 * a );
        this.k = c - ( ( b * b ) / ( 4 * a ) );

        // @public (read-only)
        this.vertex = new Vector2( this.h, this.k );
        this.focus = new Vector2( this.h, this.k + this.p );
        this.directrix = this.k - this.p; // y = directrix
        this.axisOfSymmetry = this.h; // x = h
      }
      else {
        this.k = c; // to support y = k when a === 0 in vertex form
      }
    }

    /**
     * Returns a copy of this Quadratic with a specified color.
     * @param {Color|string} color
     * @returns {Quadratic}
     * @public
     */
    withColor( color ) {
      return new Quadratic( this.a, this.b, this.c, { color: color } );
    }

    /**
     * Creates a Quadratic based on the vertex form equation y = a(x - h)^2 + k
     * @param {number} a
     * @param {number} h
     * @param {number} k
     * @param {Object} [options] - see Quadratic constructor
     * @returns {Quadratic}
     * @public
     */
    static createFromVertexForm( a, h, k, options ) {
      if ( a !== 0 ) {
        const b = -2 * a * h;
        const c = ( a * h * h ) + k;
        return new Quadratic( a, b, c, options );
      }
      else {
        // to support y = k when a === 0 in vertex form
        return new Quadratic( 0, 0, k, options );
      }
    }

    /**
     * Creates a Quadratic based on the equation y = (1/(4p))(x - h)^2 + k
     * This is an alternate vertex form, where 1/(4p) is substituted for a.
     * @param {number} p
     * @param {number} h
     * @param {number} k
     * @param {Object} [options] - see Quadratic constructor
     * @returns {Quadratic}
     * @public
     */
    static createFromAlternateVertexForm( p, h, k, options ) {
      const a = 1 / ( 4 * p );
      return Quadratic.createFromVertexForm( a, h, k, options );
    }

    /**
     * Returns a string representation of this Quadratic. For debugging only, do not rely on format!
     * @returns {string}
     * @public
     */
    toString() {
      return StringUtils.fillIn( 'Quadratic {{a}}x^2 + {{b}}x + {{c}}, color={{color}}', {
        a: this.a,
        b: this.b,
        c: this.c,
        color: this.color
      } );
    }

    /**
     * Tests whether this quadratic is equal to the given quadratic. Color is not significant.
     * @param {*} quadratic
     * @returns {boolean}
     * @public
     */
    equals( quadratic ) {
      return ( quadratic instanceof Quadratic ) &&
             ( this.a === quadratic.a ) && ( this.b === quadratic.b ) && ( this.c === quadratic.c );
    }

    /**
     * Gets the quadratic term, y = ax^2
     * @returns {Quadratic}
     * @public
     */
    getQuadraticTerm() {
      return new Quadratic( this.a, 0, 0, { color: GQColors.QUADRATIC_TERM } );
    }

    /**
     * Gets the linear term, y = bx
     * @returns {Quadratic}
     * @public
     */
    getLinearTerm() {
      return new Quadratic( 0, this.b, 0, { color: GQColors.LINEAR_TERM } );
    }

    /**
     * Gets the constant term, y = c
     * @returns {Quadratic}
     * @public
     */
    getConstantTerm() {
      return new Quadratic( 0, 0, this.c, { color: GQColors.CONSTANT_TERM } );
    }

    /**
     * Given y, solve for x.
     * @param {number} y
     * @returns {number[]} - one or more solutions
     * @public
     */
    solveX( y ) {
      if ( this.a !== 0 ) {

        // For a parabola, use vertex form.
        // y = a(x - h)^2 + k => x = h +- Math.sqrt((y - k)/a)
        // This yields 2 solutions
        const commonTerm = Math.sqrt( ( y - this.k ) / this.a );
        const x0 = this.h - commonTerm;
        const x1 = this.h + commonTerm;
        return [ x0, x1 ];
      }
      else {
        // For a straight line, use slope-intercept form.
        // y = bx + c => x = (y - c)/b
        // This yields one solution.
        const x0 = ( y - this.c ) / this.b;
        return [ x0 ];
      }
    }

    /**
     * Given x, solve for y.
     * @param {number} x
     * @returns {number}
     * @public
     */
    solveY( x ) {
      return ( this.a * x * x ) + ( this.b * x ) + this.c; // y = ax^2 + bx + c
    }

    /**
     * Gets the slope of the tangent line at point (x,f(x)) on the quadratic.
     * @param {number} x
     * @returns {number}
     */
    getTangentSlope( x ) {
      assert && assert( this.a !== 0, 'not supported non-parabola' );
      return ( 2 * this.a * x ) + this.b;
    }

    /**
     * Gets the control points for the Bezier curve that describes this quadratic.
     * See https://github.com/phetsims/graphing-quadratics/issues/1
     * @param {Range} xRange - range of the graph's x axis
     * @returns {startPoint:Vector2, controlPoint:Vector2, endPoint:Vector2}
     * @public
     */
    getControlPoints( xRange ) {

      const minX = xRange.min;
      const maxX = xRange.max;
      const length = xRange.getLength();

      const aPrime = this.a * length * length;
      const bPrime = ( 2 * this.a * minX * length ) + ( this.b * length );
      const cPrime = ( this.a * minX * minX ) + ( this.b * minX ) + this.c;

      const startPoint = new Vector2( minX, cPrime );
      const controlPoint = new Vector2( ( minX + maxX ) / 2, bPrime / 2 + cPrime );
      const endPoint = new Vector2( maxX, aPrime + bPrime + cPrime );

      return { startPoint, controlPoint, endPoint }; // caution! ES6 object shorthand
    }

    /**
     * Does a specified point lie on this quadratic?
     * @param {Vector2} point
     * @returns {boolean}
     * @public
     */
    isOnQuadratic( point ) {
      return ( Math.abs( this.solveY( point.x ) - point.y ) < EPSILON );
    }

    // TODO #22, math does not work well for cases of -1 < a < 1
    /**
     * Gets the point on this quadratic that is closest to a specified point.
     * See http://mathworld.wolfram.com/Point-QuadraticDistance.html
     * @param {Vector2} point
     * @returns {Vector2}
     * @public
     */
    getClosestPoint( point ) {

      // to improve readability
      const x = point.x;
      const y = point.y;
      const a = this.a; // a2
      const b = this.b; // a1
      const c = this.c; // a0

      //TODO use solveRoots here?
      const roots = Util.solveCubicRootsReal(
        2 * a * a,
        3 * a * b,
        b * b + 2 * a * c - 2 * a * y + 1/2, //TODO should be 1 not 1/2
        b * c - b * y - x
      );

      let rootPoint;
      let nearestPoint = new Vector2( roots[ 0 ], this.solveY( roots[ 0 ] ) );
      for ( let i = 1; i < roots.length; i ++ ) {
        rootPoint = new Vector2( roots[ i ], this.solveY( roots[ i ] ) );
        if ( rootPoint.distance( point ) < nearestPoint.distance( point ) ) {
          nearestPoint = rootPoint;
        }
      }

      return nearestPoint;
    }
  }

  /**
   * Returns the real roots of the quadratic y = ax^2 + bx + c.
   * @param {number} a
   * @param {number} b
   * @param {number} c
   * @returns {Vector2[]|null} null means that all points are roots (y = 0)
   */
  function solveRoots( a, b, c ) {
    let roots = null;
    const xCoordinates = Util.solveQuadraticRootsReal( a, b, c );
    if ( xCoordinates !== null ) {
      roots = [];
      _.uniq( xCoordinates ).forEach( x => { roots.push( new Vector2( x, 0 ) ); } );
    }
    assert && assert( roots === null || ( roots.length >= 0 && roots.length <= 2 ), 'unexpected roots: ' + roots );
    return roots;
  }

  return graphingQuadratics.register( 'Quadratic', Quadratic );
} );<commit_msg>doc<commit_after>// Copyright 2018, University of Colorado Boulder

/**
 * An immutable quadratic, described by the equation y = ax^2 + bx + c.
 *
 * Typically, a quadratic requires a !== 0. But this sim is required to support a === 0.
 * So there is some non-standard behavior herein that is not exactly mathematically correct.
 * Specifically:
 *
 * (1) When a === 0, Quadratic behaves like a straight line.  See for example solveX.
 *
 * (2) When a === 0, y = c (standard form) and y = k (vertex form) describe the same horizontal line.
 *     See for example createFromVertexForm.
 *
 * @author Andrea Lin
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( require => {
  'use strict';

  // modules
  const graphingQuadratics = require( 'GRAPHING_QUADRATICS/graphingQuadratics' );
  const GQColors = require( 'GRAPHING_QUADRATICS/common/GQColors' );
  const StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  const Util = require( 'DOT/Util' );
  const Vector2 = require( 'DOT/Vector2' );

  // constants
  const EPSILON = 1e-8;  // how close is considered to be "on" the quadratic

  class Quadratic {

    /**
     * @param {number} a - coefficient for the quadratic term
     * @param {number} b - coefficient for the linear term
     * @param {number} c - constant term
     * @param {Object} [options]
     */
    constructor( a, b, c, options ) {

      options = _.extend( {
        color: 'black' // {Color|String} color used to render the curve
      }, options );

      // @public (read-only)
      this.a = a;
      this.b = b;
      this.c = c;
      this.color = options.color;

      // @public (read-only) {Vector2[]|null} null means that all points are roots (y = 0)
      this.roots = solveRoots( a, b, c );

      // Strictly speaking, we don't have a quadratic if a === 0.
      // If that's the case, then the fields in this if block will be undefined.
      if ( a !== 0 ) {

        // @public (read-only) derive coefficients for vertex form, y = (1/(4p))(x - h)^2 + k
        this.p = 1 / ( 4 * a );
        this.h = -b / ( 2 * a );
        this.k = c - ( ( b * b ) / ( 4 * a ) );

        // @public (read-only)
        this.vertex = new Vector2( this.h, this.k );
        this.focus = new Vector2( this.h, this.k + this.p );
        this.directrix = this.k - this.p; // y = directrix
        this.axisOfSymmetry = this.h; // x = h
      }
      else {
        this.k = c; // to support y = k when a === 0 in vertex form
      }
    }

    /**
     * Returns a copy of this Quadratic with a specified color.
     * @param {Color|string} color
     * @returns {Quadratic}
     * @public
     */
    withColor( color ) {
      return new Quadratic( this.a, this.b, this.c, { color: color } );
    }

    /**
     * Creates a Quadratic based on the vertex form equation y = a(x - h)^2 + k
     * @param {number} a
     * @param {number} h
     * @param {number} k
     * @param {Object} [options] - see Quadratic constructor
     * @returns {Quadratic}
     * @public
     */
    static createFromVertexForm( a, h, k, options ) {
      if ( a !== 0 ) {
        const b = -2 * a * h;
        const c = ( a * h * h ) + k;
        return new Quadratic( a, b, c, options );
      }
      else {
        // to support y = k when a === 0 in vertex form
        return new Quadratic( 0, 0, k, options );
      }
    }

    /**
     * Creates a Quadratic based on the equation y = (1/(4p))(x - h)^2 + k
     * This is an alternate vertex form, where 1/(4p) is substituted for a.
     * @param {number} p
     * @param {number} h
     * @param {number} k
     * @param {Object} [options] - see Quadratic constructor
     * @returns {Quadratic}
     * @public
     */
    static createFromAlternateVertexForm( p, h, k, options ) {
      const a = 1 / ( 4 * p );
      return Quadratic.createFromVertexForm( a, h, k, options );
    }

    /**
     * Returns a string representation of this Quadratic. For debugging only, do not rely on format!
     * @returns {string}
     * @public
     */
    toString() {
      return StringUtils.fillIn( 'Quadratic {{a}}x^2 + {{b}}x + {{c}}, color={{color}}', {
        a: this.a,
        b: this.b,
        c: this.c,
        color: this.color
      } );
    }

    /**
     * Tests whether this quadratic is equal to the given quadratic. Color is not significant.
     * @param {*} quadratic
     * @returns {boolean}
     * @public
     */
    equals( quadratic ) {
      return ( quadratic instanceof Quadratic ) &&
             ( this.a === quadratic.a ) && ( this.b === quadratic.b ) && ( this.c === quadratic.c );
    }

    /**
     * Gets the quadratic term, y = ax^2
     * @returns {Quadratic}
     * @public
     */
    getQuadraticTerm() {
      return new Quadratic( this.a, 0, 0, { color: GQColors.QUADRATIC_TERM } );
    }

    /**
     * Gets the linear term, y = bx
     * @returns {Quadratic}
     * @public
     */
    getLinearTerm() {
      return new Quadratic( 0, this.b, 0, { color: GQColors.LINEAR_TERM } );
    }

    /**
     * Gets the constant term, y = c
     * @returns {Quadratic}
     * @public
     */
    getConstantTerm() {
      return new Quadratic( 0, 0, this.c, { color: GQColors.CONSTANT_TERM } );
    }

    /**
     * Given y, solve for x.
     * @param {number} y
     * @returns {number[]} - one or more solutions
     * @public
     */
    solveX( y ) {
      if ( this.a !== 0 ) {

        // For a parabola, use vertex form.
        // y = a(x - h)^2 + k => x = h +- Math.sqrt((y - k)/a)
        // This yields 2 solutions
        const commonTerm = Math.sqrt( ( y - this.k ) / this.a );
        const x0 = this.h - commonTerm;
        const x1 = this.h + commonTerm;
        return [ x0, x1 ];
      }
      else {
        // For a straight line, use slope-intercept form.
        // y = bx + c => x = (y - c)/b
        // This yields one solution.
        const x0 = ( y - this.c ) / this.b;
        return [ x0 ];
      }
    }

    /**
     * Given x, solve for y.
     * @param {number} x
     * @returns {number}
     * @public
     */
    solveY( x ) {
      return ( this.a * x * x ) + ( this.b * x ) + this.c; // y = ax^2 + bx + c
    }

    /**
     * Gets the slope of the tangent line at point (x,f(x)) on the quadratic.
     * @param {number} x
     * @returns {number}
     */
    getTangentSlope( x ) {
      assert && assert( this.a !== 0, 'not supported non-parabola' );
      return ( 2 * this.a * x ) + this.b; // first derivative
    }

    /**
     * Gets the control points for the Bezier curve that describes this quadratic.
     * See https://github.com/phetsims/graphing-quadratics/issues/1
     * @param {Range} xRange - range of the graph's x axis
     * @returns {startPoint:Vector2, controlPoint:Vector2, endPoint:Vector2}
     * @public
     */
    getControlPoints( xRange ) {

      const minX = xRange.min;
      const maxX = xRange.max;
      const length = xRange.getLength();

      const aPrime = this.a * length * length;
      const bPrime = ( 2 * this.a * minX * length ) + ( this.b * length );
      const cPrime = ( this.a * minX * minX ) + ( this.b * minX ) + this.c;

      const startPoint = new Vector2( minX, cPrime );
      const controlPoint = new Vector2( ( minX + maxX ) / 2, bPrime / 2 + cPrime );
      const endPoint = new Vector2( maxX, aPrime + bPrime + cPrime );

      return { startPoint, controlPoint, endPoint }; // caution! ES6 object shorthand
    }

    /**
     * Does a specified point lie on this quadratic?
     * @param {Vector2} point
     * @returns {boolean}
     * @public
     */
    isOnQuadratic( point ) {
      return ( Math.abs( this.solveY( point.x ) - point.y ) < EPSILON );
    }

    // TODO #22, math does not work well for cases of -1 < a < 1
    /**
     * Gets the point on this quadratic that is closest to a specified point.
     * See http://mathworld.wolfram.com/Point-QuadraticDistance.html
     * @param {Vector2} point
     * @returns {Vector2}
     * @public
     */
    getClosestPoint( point ) {

      // to improve readability
      const x = point.x;
      const y = point.y;
      const a = this.a; // a2
      const b = this.b; // a1
      const c = this.c; // a0

      //TODO use solveRoots here?
      const roots = Util.solveCubicRootsReal(
        2 * a * a,
        3 * a * b,
        b * b + 2 * a * c - 2 * a * y + 1/2, //TODO should be 1 not 1/2
        b * c - b * y - x
      );

      let rootPoint;
      let nearestPoint = new Vector2( roots[ 0 ], this.solveY( roots[ 0 ] ) );
      for ( let i = 1; i < roots.length; i ++ ) {
        rootPoint = new Vector2( roots[ i ], this.solveY( roots[ i ] ) );
        if ( rootPoint.distance( point ) < nearestPoint.distance( point ) ) {
          nearestPoint = rootPoint;
        }
      }

      return nearestPoint;
    }
  }

  /**
   * Returns the real roots of the quadratic y = ax^2 + bx + c.
   * @param {number} a
   * @param {number} b
   * @param {number} c
   * @returns {Vector2[]|null} null means that all points are roots (y = 0)
   */
  function solveRoots( a, b, c ) {
    let roots = null;
    const xCoordinates = Util.solveQuadraticRootsReal( a, b, c );
    if ( xCoordinates !== null ) {
      roots = [];
      _.uniq( xCoordinates ).forEach( x => { roots.push( new Vector2( x, 0 ) ); } );
    }
    assert && assert( roots === null || ( roots.length >= 0 && roots.length <= 2 ), 'unexpected roots: ' + roots );
    return roots;
  }

  return graphingQuadratics.register( 'Quadratic', Quadratic );
} );<|endoftext|>


<commit_before>describe('formInput', function () {
    var elm, scope;

    // load the form code
    beforeEach(angular.mock.module('formsAngular'));

    describe('simple text input', function () {

        beforeEach(inject(function ($rootScope, $compile) {
            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact">' +
                    '<form-input info="{{formSchema}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.formSchema = {name: "desc", id: "desc_id", label: "Description", type: "text"};
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have a label', function () {
            var label = elm.find('label');
            expect(label.text()).toBe('Description');
            expect(label.attr('for')).toBe('desc_id');
            expect(label).toHaveClass('control-label');
        });

        it('should have input', function () {
            var input = elm.find('input');
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-valid');
            expect(input.attr('id')).toBe('desc_id');
            expect(input.attr('type')).toBe('text');
        });

    });

    describe('generate inputs from schema', function () {

        beforeEach(inject(function ($rootScope, $controller, $compile) {

            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact"> ' +
                    '<form-input ng-repeat="field in formSchema" info="{{field}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.formSchema = [
                {name: "name", id: "1", label: "Name", type: "text"},
                {name: "eyecolour", id: "2", label: "Colour of Eyes", type: "text"}
            ];
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have 2 inputs', function () {
            var input = elm.find('input');
            expect(input.length).toBe(2);
            input = elm.find('input:first');
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-valid');
            expect(input.attr('id')).toBe('1');
            expect(input.attr('autofocus')).toBe('autofocus');
            expect(input.attr('type')).toBe('text');
            input = elm.find('input:last');
            expect(input.attr('id')).toBe('2');
            expect(input.attr('type')).toBe('text');
            expect(input.attr('autofocus')).toBe(undefined);
        });

        it('should have 2 labels', function () {
            var label = elm.find('label');
            expect(label.length).toBe(2);
            label = elm.find('label:first');
            expect((label).text()).toBe('Name');
            expect(label.attr('for')).toBe('1');
            label = elm.find('label:last');
            expect(label.text()).toBe('Colour of Eyes');
            expect(label.attr('for')).toBe('2');
        });

    });

    describe('handles sub schemas', function() {

        describe('default behaviour', function() {
            beforeEach(inject(function ($rootScope, $controller, $compile) {
                elm = angular.element(
                    '<form name="myForm" class="form-horizontal compact"> ' +
                        '<form-input ng-repeat="field in formSchema" info="{{field}}"></form-input>' +
                        '</form>');

                scope = $rootScope;
                scope.formSchema = [
                    {"name":"surname","id":"f_surname","label":"Surname","type":"text"},
                    {"name":"forename","id":"f_forename","label":"Forename","type":"text"},
                    {"name":"exams","id":"f_exams","label":"Exams","schema":[
                        {"name":"exams.subject","id":"f_exams.subject","label":"Subject","type":"text"},
                        {"name":"exams.score","id":"f_exams.score","label":"Score","type":"number"}
                        ]}
                ];

                scope.record = {"surname":"Smith","forename":"Anne","exams":[{"subject":"English","score":83},{"subject":"Maths","score":97}]};
                $compile(elm)(scope);
                scope.$digest();
            }));

            it('has a heading for the Exams section', function() {
                var thisElm = elm.find('.schema-head');
                expect(thisElm.length).toBe(1);
                expect((thisElm).text()).toBe('Exams');

                thisElm = elm.find('.schema-foot');
                expect(thisElm.length).toBe(1);

                thisElm = elm.find('.schema-foot button');
                expect(thisElm.length).toBe(1);
                expect((thisElm).text()).toBe(' Add');

                thisElm = elm.find('.sub-doc')
                expect(thisElm.length).toBe(2);

                thisElm = elm.find('.sub-doc button:first');
                expect(thisElm.text()).toBe(' Remove');
            });
        });

        describe('Inhibit add and remove', function() {
            beforeEach(inject(function ($rootScope, $controller, $compile) {
                elm = angular.element(
                    '<form name="myForm" class="form-horizontal compact"> ' +
                        '<form-input ng-repeat="field in formSchema" info="{{field}}"></form-input>' +
                        '</form>');

                scope = $rootScope;
                scope.formSchema = [
                    {"name":"surname","id":"f_surname","label":"Surname","type":"text"},
                    {"name":"forename","id":"f_forename","label":"Forename","type":"text"},
                    {"name":"exams","id":"f_exams","label":"Exams", "noAdd":true, "noRemove":true, "schema":[
                        {"name":"exams.subject","id":"f_exams.subject","label":"Subject","type":"text"},
                        {"name":"exams.score","id":"f_exams.score","label":"Score","type":"number"}
                    ]}
                ];

                scope.record = {"surname":"Smith","forename":"Anne","exams":[{"subject":"English","score":83},{"subject":"Maths","score":97}]};
                $compile(elm)(scope);
                scope.$digest();
            }));

            it('has a heading for the Exams section', function() {
                var thisElm = elm.find('.schema-head');
                expect(thisElm.length).toBe(1);
                expect((thisElm).text()).toBe('Exams');

                thisElm = elm.find('.schema-foot');
                expect(thisElm.length).toBe(1);

                thisElm = elm.find('.schema-foot button');
                expect(thisElm.length).toBe(0);

                thisElm = elm.find('.sub-doc')
                expect(thisElm.length).toBe(2);

                thisElm = elm.find('.sub-doc button:first');
                expect(thisElm.length).toBe(0);
            });
        });

    })

    describe('does not generate label element when label is blank', function () {

        beforeEach(inject(function ($rootScope, $controller, $compile) {

            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact"> ' +
                    '<form-input ng-repeat="field in schema" info="{{field}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.schema = [
                {name: "name", id: "1", label: "Name", type: "text"},
                {name: "eyecolour", id: "2", label: "", type: "text"}
            ];
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have 2 inputs', function () {
            var input = elm.find('input');
            expect(input.length).toBe(2);
            input = elm.find('input:first');
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-valid');
            expect(input.attr('id')).toBe('1');
            expect(input.attr('type')).toBe('text');
            input = elm.find('input:last');
            expect(input.attr('id')).toBe('2');
            expect(input.attr('type')).toBe('text');
        });

        it('should have 1 label', function () {
            var label = elm.find('label');
            expect(label.length).toBe(1);
            expect((label).text()).toBe('Name');
            expect(label.attr('for')).toBe('1');
        });

    });

    describe('generates "required" attribute when appropriate', function () {

        beforeEach(inject(function ($rootScope, $controller, $compile) {

            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact"> ' +
                    '<form-input ng-repeat="field in schema" info="{{field}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.schema = [
                {name: "name", id: "1", label: "Name", type: "text", required: true},
                {name: "eyecolour", id: "2", label: "", type: "text"}
            ];
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have correct inputs', function () {
            var input = elm.find('input');
            expect(input.length).toBe(2);
            input = elm.find('input:first');
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-invalid');
            expect(input.attr('id')).toBe('1');
            expect(input.attr('required')).toBe("required");
            expect(input.attr('type')).toBe('text');
            input = elm.find('input:last');
            expect(input.attr('id')).toBe('2');
            expect(input.attr('required')).toBe(undefined);
            expect(input.attr('type')).toBe('text');
        });

    });

    describe('generates help elements', function () {

        beforeEach(inject(function ($rootScope, $controller, $compile) {

            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact"> ' +
                    '<form-input ng-repeat="field in schema" info="{{field}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.schema = [
                {name: "name", id: "1", label: "Name", type: "text", help: "This is some help"},
                {name: "eyecolour", id: "2", label: "", type: "text", helpInline: "This is some inline help"}
            ];
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have correct help blocks', function () {
            var help = elm.find('span');
            expect(help.length).toBe(2);
            help = elm.find('span:first');
            expect(help).toHaveClass('help-block');
            expect(help.text()).toBe('This is some help');
            help = elm.find('span:last');
            expect(help).toHaveClass('help-inline');
            expect(help.text()).toBe('This is some inline help');
        });

    });

    describe('generates textarea inputs', function () {

        beforeEach(inject(function ($rootScope, $controller, $compile) {

            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact"> ' +
                    '<form-input ng-repeat="field in schema" info="{{field}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.schema = [
                {name: "name", id: "1", label: "Name", type: "text"},
                {name: "description", id: "2", label: "Desc", type: "textarea", rows: 10}
            ];
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have correct textarea', function () {
            var input = elm.find('input');
            expect(input.length).toBe(1);
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-valid');
            expect(input.attr('id')).toBe('1');
            expect(input.attr('type')).toBe('text');
            input = elm.find('textarea');
            expect(input.length).toBe(1);
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-valid');
            expect(input.attr('id')).toBe('2');
            expect(input.attr('rows')).toBe('10');
        });

    });

    describe('generates selects for enumerated lists', function () {

        beforeEach(inject(function ($rootScope, $controller, $compile) {

            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact"> ' +
                    '<form-input ng-repeat="field in schema" info="{{field}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.f_eyeColourOptions = ["Blue", "Brown", "Green", "Hazel"];
            scope.schema = [
                {name: "name", id: "1", label: "Name", type: "text"},
                {"name": "eyeColour", "id": "f_eyeColour", "label": "Eye Colour", "type": "select", "options": "f_eyeColourOptions"}
            ];
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have combobox', function () {
            var input = elm.find('select');
            expect(input.length).toBe(1);
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-valid');
            expect(input.attr('id')).toBe('f_eyeColour');
            input = elm.find('option');
            expect(input.length).toBe(5);
            input = elm.find('option:first');
            expect(input.attr('value')).toBe("");
            expect(input.text()).toBe("");
            input = elm.find('option:last');
            expect(input.attr('value')).toBe("Hazel");
            expect(input.text()).toBe("Hazel");
        });

    });

    describe('generates selects for reference lookups', function () {

        beforeEach(inject(function ($rootScope, $controller, $compile) {

            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact"> ' +
                    '<form-input ng-repeat="field in schema" info="{{field}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.f_eyeColourOptions = ["Blue", "Brown", "Green", "Hazel"];
            scope.f_eyeColour_ids = ["1234", "5678", "90ab", "cdef"];
            scope.schema = [
                {name: "name", id: "1", label: "Name", type: "text"},
                {"name": "eyeColour", "id": "f_eyeColour", "label": "Eye Colour", "type": "select", "options": "f_eyeColourOptions", "ids": "f_eyeColour_ids"}
            ];
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have combobox', function () {
            var input = elm.find('select');
            expect(input.length).toBe(1);
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-valid');
            expect(input.attr('id')).toBe('f_eyeColour');
            input = elm.find('option');
            expect(input.length).toBe(5);
            input = elm.find('option:first');
            expect(input.attr('value')).toBe("");
            expect(input.text()).toBe("");
            input = elm.find('option:last');
            expect(input.text()).toBe("Hazel");
        });

    });

    describe('displays error messages', function () {

        beforeEach(
            inject(function ($rootScope, $controller, $compile) {
                elm = angular.element(
                    '
'+
                        '
'+
                            '<button type="button" class="close" ng-click="dismissError()">×</button>'+
                            '
{{alertTitle}}
'+
                            '{{errorMessage}}'+
                        '
'+
                    '
'+
                        '
'+
                           '<form-input ng-repeat="field in formSchema" info="{{field}}"></form-input>'+
                        '
'+
                    '
');
                scope = $rootScope;
                scope.schema = [
                    {name: "email", id: "1", label: "Email", type: "text", directive: "email-field"}
                ];
                scope.errorMessage = 'Test error';
                scope.alertTitle = 'Error!';
                $compile(elm)(scope);
                scope.$digest();
            }));

        it('displays appropriately', function () {
            var h4 = elm.find('h4');
            expect(h4.text()).toBe('Error!');
            var alert = elm.find('span.errMsg');
            expect(alert.text()).toBe('Test error');
        });
    });

});

<commit_msg>Change test desciption<commit_after>describe('formInput', function () {
    var elm, scope;

    // load the form code
    beforeEach(angular.mock.module('formsAngular'));

    describe('simple text input', function () {

        beforeEach(inject(function ($rootScope, $compile) {
            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact">' +
                    '<form-input info="{{formSchema}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.formSchema = {name: "desc", id: "desc_id", label: "Description", type: "text"};
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have a label', function () {
            var label = elm.find('label');
            expect(label.text()).toBe('Description');
            expect(label.attr('for')).toBe('desc_id');
            expect(label).toHaveClass('control-label');
        });

        it('should have input', function () {
            var input = elm.find('input');
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-valid');
            expect(input.attr('id')).toBe('desc_id');
            expect(input.attr('type')).toBe('text');
        });

    });

    describe('generate inputs from schema', function () {

        beforeEach(inject(function ($rootScope, $controller, $compile) {

            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact"> ' +
                    '<form-input ng-repeat="field in formSchema" info="{{field}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.formSchema = [
                {name: "name", id: "1", label: "Name", type: "text"},
                {name: "eyecolour", id: "2", label: "Colour of Eyes", type: "text"}
            ];
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have 2 inputs', function () {
            var input = elm.find('input');
            expect(input.length).toBe(2);
            input = elm.find('input:first');
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-valid');
            expect(input.attr('id')).toBe('1');
            expect(input.attr('autofocus')).toBe('autofocus');
            expect(input.attr('type')).toBe('text');
            input = elm.find('input:last');
            expect(input.attr('id')).toBe('2');
            expect(input.attr('type')).toBe('text');
            expect(input.attr('autofocus')).toBe(undefined);
        });

        it('should have 2 labels', function () {
            var label = elm.find('label');
            expect(label.length).toBe(2);
            label = elm.find('label:first');
            expect((label).text()).toBe('Name');
            expect(label.attr('for')).toBe('1');
            label = elm.find('label:last');
            expect(label.text()).toBe('Colour of Eyes');
            expect(label.attr('for')).toBe('2');
        });

    });

    describe('handles sub schemas', function() {

        describe('default behaviour', function() {
            beforeEach(inject(function ($rootScope, $controller, $compile) {
                elm = angular.element(
                    '<form name="myForm" class="form-horizontal compact"> ' +
                        '<form-input ng-repeat="field in formSchema" info="{{field}}"></form-input>' +
                        '</form>');

                scope = $rootScope;
                scope.formSchema = [
                    {"name":"surname","id":"f_surname","label":"Surname","type":"text"},
                    {"name":"forename","id":"f_forename","label":"Forename","type":"text"},
                    {"name":"exams","id":"f_exams","label":"Exams","schema":[
                        {"name":"exams.subject","id":"f_exams.subject","label":"Subject","type":"text"},
                        {"name":"exams.score","id":"f_exams.score","label":"Score","type":"number"}
                        ]}
                ];

                scope.record = {"surname":"Smith","forename":"Anne","exams":[{"subject":"English","score":83},{"subject":"Maths","score":97}]};
                $compile(elm)(scope);
                scope.$digest();
            }));

            it('has Exams section', function() {
                var thisElm = elm.find('.schema-head');
                expect(thisElm.length).toBe(1);
                expect((thisElm).text()).toBe('Exams');

                thisElm = elm.find('.schema-foot');
                expect(thisElm.length).toBe(1);

                thisElm = elm.find('.schema-foot button');
                expect(thisElm.length).toBe(1);
                expect((thisElm).text()).toBe(' Add');

                thisElm = elm.find('.sub-doc')
                expect(thisElm.length).toBe(2);

                thisElm = elm.find('.sub-doc button:first');
                expect(thisElm.text()).toBe(' Remove');
            });
        });

        describe('Inhibit add and remove', function() {
            beforeEach(inject(function ($rootScope, $controller, $compile) {
                elm = angular.element(
                    '<form name="myForm" class="form-horizontal compact"> ' +
                        '<form-input ng-repeat="field in formSchema" info="{{field}}"></form-input>' +
                        '</form>');

                scope = $rootScope;
                scope.formSchema = [
                    {"name":"surname","id":"f_surname","label":"Surname","type":"text"},
                    {"name":"forename","id":"f_forename","label":"Forename","type":"text"},
                    {"name":"exams","id":"f_exams","label":"Exams", "noAdd":true, "noRemove":true, "schema":[
                        {"name":"exams.subject","id":"f_exams.subject","label":"Subject","type":"text"},
                        {"name":"exams.score","id":"f_exams.score","label":"Score","type":"number"}
                    ]}
                ];

                scope.record = {"surname":"Smith","forename":"Anne","exams":[{"subject":"English","score":83},{"subject":"Maths","score":97}]};
                $compile(elm)(scope);
                scope.$digest();
            }));

            it('has amended Exams section', function() {
                var thisElm = elm.find('.schema-head');
                expect(thisElm.length).toBe(1);
                expect((thisElm).text()).toBe('Exams');

                thisElm = elm.find('.schema-foot');
                expect(thisElm.length).toBe(1);

                thisElm = elm.find('.schema-foot button');
                expect(thisElm.length).toBe(0);

                thisElm = elm.find('.sub-doc')
                expect(thisElm.length).toBe(2);

                thisElm = elm.find('.sub-doc button:first');
                expect(thisElm.length).toBe(0);
            });
        });

    })

    describe('does not generate label element when label is blank', function () {

        beforeEach(inject(function ($rootScope, $controller, $compile) {

            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact"> ' +
                    '<form-input ng-repeat="field in schema" info="{{field}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.schema = [
                {name: "name", id: "1", label: "Name", type: "text"},
                {name: "eyecolour", id: "2", label: "", type: "text"}
            ];
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have 2 inputs', function () {
            var input = elm.find('input');
            expect(input.length).toBe(2);
            input = elm.find('input:first');
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-valid');
            expect(input.attr('id')).toBe('1');
            expect(input.attr('type')).toBe('text');
            input = elm.find('input:last');
            expect(input.attr('id')).toBe('2');
            expect(input.attr('type')).toBe('text');
        });

        it('should have 1 label', function () {
            var label = elm.find('label');
            expect(label.length).toBe(1);
            expect((label).text()).toBe('Name');
            expect(label.attr('for')).toBe('1');
        });

    });

    describe('generates "required" attribute when appropriate', function () {

        beforeEach(inject(function ($rootScope, $controller, $compile) {

            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact"> ' +
                    '<form-input ng-repeat="field in schema" info="{{field}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.schema = [
                {name: "name", id: "1", label: "Name", type: "text", required: true},
                {name: "eyecolour", id: "2", label: "", type: "text"}
            ];
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have correct inputs', function () {
            var input = elm.find('input');
            expect(input.length).toBe(2);
            input = elm.find('input:first');
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-invalid');
            expect(input.attr('id')).toBe('1');
            expect(input.attr('required')).toBe("required");
            expect(input.attr('type')).toBe('text');
            input = elm.find('input:last');
            expect(input.attr('id')).toBe('2');
            expect(input.attr('required')).toBe(undefined);
            expect(input.attr('type')).toBe('text');
        });

    });

    describe('generates help elements', function () {

        beforeEach(inject(function ($rootScope, $controller, $compile) {

            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact"> ' +
                    '<form-input ng-repeat="field in schema" info="{{field}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.schema = [
                {name: "name", id: "1", label: "Name", type: "text", help: "This is some help"},
                {name: "eyecolour", id: "2", label: "", type: "text", helpInline: "This is some inline help"}
            ];
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have correct help blocks', function () {
            var help = elm.find('span');
            expect(help.length).toBe(2);
            help = elm.find('span:first');
            expect(help).toHaveClass('help-block');
            expect(help.text()).toBe('This is some help');
            help = elm.find('span:last');
            expect(help).toHaveClass('help-inline');
            expect(help.text()).toBe('This is some inline help');
        });

    });

    describe('generates textarea inputs', function () {

        beforeEach(inject(function ($rootScope, $controller, $compile) {

            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact"> ' +
                    '<form-input ng-repeat="field in schema" info="{{field}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.schema = [
                {name: "name", id: "1", label: "Name", type: "text"},
                {name: "description", id: "2", label: "Desc", type: "textarea", rows: 10}
            ];
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have correct textarea', function () {
            var input = elm.find('input');
            expect(input.length).toBe(1);
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-valid');
            expect(input.attr('id')).toBe('1');
            expect(input.attr('type')).toBe('text');
            input = elm.find('textarea');
            expect(input.length).toBe(1);
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-valid');
            expect(input.attr('id')).toBe('2');
            expect(input.attr('rows')).toBe('10');
        });

    });

    describe('generates selects for enumerated lists', function () {

        beforeEach(inject(function ($rootScope, $controller, $compile) {

            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact"> ' +
                    '<form-input ng-repeat="field in schema" info="{{field}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.f_eyeColourOptions = ["Blue", "Brown", "Green", "Hazel"];
            scope.schema = [
                {name: "name", id: "1", label: "Name", type: "text"},
                {"name": "eyeColour", "id": "f_eyeColour", "label": "Eye Colour", "type": "select", "options": "f_eyeColourOptions"}
            ];
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have combobox', function () {
            var input = elm.find('select');
            expect(input.length).toBe(1);
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-valid');
            expect(input.attr('id')).toBe('f_eyeColour');
            input = elm.find('option');
            expect(input.length).toBe(5);
            input = elm.find('option:first');
            expect(input.attr('value')).toBe("");
            expect(input.text()).toBe("");
            input = elm.find('option:last');
            expect(input.attr('value')).toBe("Hazel");
            expect(input.text()).toBe("Hazel");
        });

    });

    describe('generates selects for reference lookups', function () {

        beforeEach(inject(function ($rootScope, $controller, $compile) {

            elm = angular.element(
                '<form name="myForm" class="form-horizontal compact"> ' +
                    '<form-input ng-repeat="field in schema" info="{{field}}"></form-input>' +
                    '</form>');

            scope = $rootScope;
            scope.f_eyeColourOptions = ["Blue", "Brown", "Green", "Hazel"];
            scope.f_eyeColour_ids = ["1234", "5678", "90ab", "cdef"];
            scope.schema = [
                {name: "name", id: "1", label: "Name", type: "text"},
                {"name": "eyeColour", "id": "f_eyeColour", "label": "Eye Colour", "type": "select", "options": "f_eyeColourOptions", "ids": "f_eyeColour_ids"}
            ];
            $compile(elm)(scope);
            scope.$digest();
        }));

        it('should have combobox', function () {
            var input = elm.find('select');
            expect(input.length).toBe(1);
            expect(input).toHaveClass('ng-pristine');
            expect(input).toHaveClass('ng-valid');
            expect(input.attr('id')).toBe('f_eyeColour');
            input = elm.find('option');
            expect(input.length).toBe(5);
            input = elm.find('option:first');
            expect(input.attr('value')).toBe("");
            expect(input.text()).toBe("");
            input = elm.find('option:last');
            expect(input.text()).toBe("Hazel");
        });

    });

    describe('displays error messages', function () {

        beforeEach(
            inject(function ($rootScope, $controller, $compile) {
                elm = angular.element(
                    '
'+
                        '
'+
                            '<button type="button" class="close" ng-click="dismissError()">×</button>'+
                            '
{{alertTitle}}
'+
                            '{{errorMessage}}'+
                        '
'+
                    '
'+
                        '
'+
                           '<form-input ng-repeat="field in formSchema" info="{{field}}"></form-input>'+
                        '
'+
                    '
');
                scope = $rootScope;
                scope.schema = [
                    {name: "email", id: "1", label: "Email", type: "text", directive: "email-field"}
                ];
                scope.errorMessage = 'Test error';
                scope.alertTitle = 'Error!';
                $compile(elm)(scope);
                scope.$digest();
            }));

        it('displays appropriately', function () {
            var h4 = elm.find('h4');
            expect(h4.text()).toBe('Error!');
            var alert = elm.find('span.errMsg');
            expect(alert.text()).toBe('Test error');
        });
    });

});

<|endoftext|>


<commit_before>/*
 * Copyright (C) 2015 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import $ from 'jquery'
import createStore from 'jsx/shared/helpers/createStore'
import parseLinkHeader from 'compiled/fn/parseLinkHeader'
import _ from 'underscore'
import 'jquery.ajaxJSON'

  /**
   * Build a store that support basic ajax fetching (first, next, all),
   * and caches the results by params.
   *
   * You only need to implement getUrl, and can optionally implement
   * normalizeParams and jsonKey
   */
  var factory = function(spec) {
    return _.extend(createStore(), {
      /**
       * Get a blank state in the store; useful when mounting the top-
       * level component that uses the store
       *
       * @param {any} context
       *        User-defined data you can use later on in normalizeParams
       *        and getUrl; will be available as `this.context`
       */
      reset(context) {
        this.clearState();
        this.context = context;
      },

      getKey(params) {
        return JSON.stringify(params || {});
      },

      normalizeParams(params) {
        return params;
      },

      getUrl() {
        throw "not implemented"
      },

      /**
       * If the API response is an object instead of an array, use this
       * to specify the key containing the actual array of results
       */
      jsonKey: null,

      /**
       * Load the first page of data for the given params
       */
      load(params) {
        var key = this.getKey(params);
        this.lastParams = params;
        var params = this.normalizeParams(params);
        var url = this.getUrl();
        var state = this.getState()[key] || {};

        return this._load(key, url, params);
      },

      /**
       * Create a record; since we're lazy, just blow away all the store
       * data, but reload the last thing we fetched
       */
      create(params) {
        var url = this.getUrl();
        return $.ajaxJSON(url, "POST", this.normalizeParams(params)).then(() => {
          this.clearState();
          if (this.lastParams)
            this.load(this.lastParams);
        });
      },

      /**
       * Load the next page of data for the given params
       */
      loadMore(params) {
        var key = this.getKey(params);
        this.lastParams = params;
        var state = this.getState()[key] || {};

        if (!state.next) return;

        return this._load(key, state.next, {}, {append: true});
      },

      /**
       * Load data from the endpoint, following `next` links until
       * everything has been fetched. Don't be dumb and call this
       * on users or something :P
       */
      loadAll(params, append) {
        var key = this.getKey(params);
        var params = this.normalizeParams(params);
        this.lastParams = params;
        var url = this.getUrl();
        this._loadAll(key, url, params, append);
      },

      _loadAll(key, url, params, append) {
        var promise = this._load(key, url, params, {append});
        if (!promise) return;

        promise.then(() => {
          var state = this.getState()[key] || {};
          if (state.next) {
            this._loadAll(key, state.next, {}, true);
          }
        });
      },

      _load(key, url, params, options) {
        options = options || {};
        this.mergeState(key, {loading: true});

        return $.ajaxJSON(url, "GET", params).then((data, _, xhr) => {
          if (this.jsonKey) {
            data = data[this.jsonKey];
          }
          if (options.wrap) {
            data = [data];
          }
          if (options.append) {
            data = (this.getStateFor(key).data || []).concat(data);
          }

          var { next } = parseLinkHeader(xhr);
          this.mergeState(key, { data, next, loading: false });
        }, (xhr) => {
          this.mergeState(key, { error: true, loading: false });
        });
      },

      getStateFor(key) {
        return this.getState()[key] || {};
      },

      mergeState(key, newState) {
        var state = this.getState()[key] || {};
        var overallState = {};
        overallState[key] = _.extend({}, state, newState);
        this.setState(overallState);
      },

      /**
       * Return whatever results we have for the given params, as well as
       * useful meta data.
       *
       * @return {Object}   obj
       *
       * @return {Object[]} obj.data
       *         The actual data
       *
       * @return {Boolean}  obj.error
       *         Indication of whether there was an error
       *
       * @return {Boolean}  obj.loading
       *         Whether or not we are currently fetching data
       *
       * @return {String}   obj.next
       *         A URL where we can retrieve the next page of data (if
       *         there is more)
       */
      get(params) {
        var key = this.getKey(params);
        return this.getState()[key];
      }
    }, spec);
  }

export default factory
<commit_msg>eslint --fix …user_search/createStore.js<commit_after>/*
 * Copyright (C) 2015 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import $ from 'jquery'
import createStore from 'jsx/shared/helpers/createStore'
import parseLinkHeader from 'compiled/fn/parseLinkHeader'
import _ from 'underscore'
import 'jquery.ajaxJSON'

/**
 * Build a store that support basic ajax fetching (first, next, all),
 * and caches the results by params.
 *
 * You only need to implement getUrl, and can optionally implement
 * normalizeParams and jsonKey
 */
export default function factory(spec) {
  return _.extend(createStore(), {
    /**
     * Get a blank state in the store; useful when mounting the top-
     * level component that uses the store
     *
     * @param {any} context
     *        User-defined data you can use later on in normalizeParams
     *        and getUrl; will be available as `this.context`
     */
    reset(context) {
      this.clearState()
      this.context = context
    },

    getKey: params => JSON.stringify(params || {}),
    normalizeParams: params => params,

    getUrl() {
      throw new Error('not implemented')
    },

    /**
     * If the API response is an object instead of an array, use this
     * to specify the key containing the actual array of results
     */
    jsonKey: null,

    /**
     * Load the first page of data for the given params
     */
    load(params) {
      const key = this.getKey(params)
      this.lastParams = params
      const normalizedParams = this.normalizeParams(params)
      const url = this.getUrl()

      return this._load(key, url, normalizedParams)
    },

    /**
     * Create a record; since we're lazy, just blow away all the store
     * data, but reload the last thing we fetched
     */
    create(params) {
      const url = this.getUrl()
      return $.ajaxJSON(url, 'POST', this.normalizeParams(params)).then(() => {
        this.clearState()
        if (this.lastParams) this.load(this.lastParams)
      })
    },

    /**
     * Load the next page of data for the given params
     */
    loadMore(params) {
      const key = this.getKey(params)
      this.lastParams = params
      const state = this.getState()[key] || {}

      if (!state.next) return

      return this._load(key, state.next, {}, {append: true})
    },

    /**
     * Load data from the endpoint, following `next` links until
     * everything has been fetched. Don't be dumb and call this
     * on users or something :P
     */
    loadAll(params, append) {
      const key = this.getKey(params)
      const normalizedParams = this.lastParams = this.normalizeParams(params)
      const url = this.getUrl()
      this._loadAll(key, url, normalizedParams, append)
    },

    _loadAll(key, url, params, append) {
      const promise = this._load(key, url, params, {append})
      if (!promise) return

      promise.then(() => {
        const state = this.getState()[key] || {}
        if (state.next) {
          this._loadAll(key, state.next, {}, true)
        }
      })
    },

    _load(key, url, params, options={}) {
      this.mergeState(key, {loading: true})

      return $.ajaxJSON(url, 'GET', params).then((data, _textStatus, xhr) => {
        if (this.jsonKey) data = data[this.jsonKey]
        if (options.wrap) data = [data]
        if (options.append) data = (this.getStateFor(key).data || []).concat(data)

        const {next} = parseLinkHeader(xhr)
        this.mergeState(key, {data, next, loading: false})
      }, () => {
        this.mergeState(key, {error: true, loading: false})
      })
    },

    getStateFor(key) {
      return this.getState()[key] || {}
    },

    mergeState(key, newState) {
      this.setState({
        [key]: {
          ...this.getStateFor(key),
          ...newState
        }
      })
    },

    /**
     * Return whatever results we have for the given params, as well as
     * useful meta data.
     *
     * @return {Object}   obj
     *
     * @return {Object[]} obj.data
     *         The actual data
     *
     * @return {Boolean}  obj.error
     *         Indication of whether there was an error
     *
     * @return {Boolean}  obj.loading
     *         Whether or not we are currently fetching data
     *
     * @return {String}   obj.next
     *         A URL where we can retrieve the next page of data (if
     *         there is more)
     */
    get(params) {
      const key = this.getKey(params)
      return this.getStateFor(key)
    }
  }, spec)
}
<|endoftext|>