<commit_before>/*!
 * Module dependencies
 */

// Third party modules
const mongoose = require('mongoose');
// var userPlugin = require('mongoose-user');
const QueryPlugin = require('mongoose-query');
const logger = require('../tools/logger');
const _ = require('lodash');

// Local components
const FileSchema = require('./extends/file');
const tools = require('../tools');

const Schema = mongoose.Schema;
const checksum = tools.checksum;
const filedb = tools.filedb;
const fileProvider = filedb.provider;
const Build = mongoose.model('Build');

FileSchema.add({
  ref: {
    type: Schema.Types.ObjectId,
    ref: 'Resource'
  },
  from: {
    type: String,
    enum: ['dut', 'framework', 'env', 'other']
  }
});

/**
 * User schema
 */
const ResultSchema = new Schema({
  tcid: {type: String, required: true, index: true},
  tcRef: {type: Schema.Types.ObjectId, ref: 'Testcase'},
  job: {
    id: {type: String, default: ''}
  },
  campaign: {type: String, default: '', index: true},
  campaignRef: {type: Schema.Types.ObjectId, ref: 'Campaign'},
  cre: {
    time: {type: Date, default: Date.now, index: true},
    user: {type: String},
    userRef: {type: Schema.Types.ObjectId, ref: 'User'}
  },
  exec: {
    verdict: {
      type: String,
      required: true,
      enum: ['pass', 'fail', 'inconclusive', 'blocked', 'error'],
      index: true
    },
    note: {type: String, default: ''},
    duration: {type: Number}, // seconds
    profiling: {type: Schema.Types.Mixed},
    env: { // environment information
      ref: {type: Schema.Types.ObjectId, ref: 'Resource'},
      rackId: {type: String},
      framework: {
        name: {type: String, default: ''},
        ver: {type: String, default: ''}
      }
    },
    sut: { // software under test
      ref: {type: Schema.Types.ObjectId, ref: 'Build'},
      gitUrl: {type: String, default: ''},
      buildName: {type: String},
      buildDate: {type: Date},
      buildUrl: {type: String, default: ''},
      buildSha1: {type: String},
      branch: {type: String, default: ''},
      commitId: {type: String, default: ''},
      tag: [{type: String}],
      href: {type: String},
      cut: [{type: String}], // Component Under Test
      fut: [{type: String}] // Feature Under Test
    },
    dut: { // Device(s) Under Test
      count: {type: Number},
      type: {type: String, enum: ['hw', 'simulator', 'process']},
      ref: {type: Schema.Types.ObjectId, ref: 'Resource'},
      vendor: {type: String},
      model: {type: String},
      ver: {type: String},
      sn: {type: String}
    },
    logs: [FileSchema]
  }
});

ResultSchema.set('toObject', {virtuals: true});

/**
 * Query plugin
 */
ResultSchema.plugin(QueryPlugin); // install QueryPlugin

/**
 * Add your
 * - pre-save hooks
 * - validations
 * - virtuals
 */

/**
 * Methods
 */
ResultSchema.pre('validate', function preValidate(next) {
  let error;
  const buildSha1 = _.get(this, 'exec.sut.buildSha1');

  const logs = _.get(this, 'exec.logs');
  if (Array.isArray(logs)) {
    for (let i = 0; i < logs.length; i += 1) {
      const file = logs[i];
      if (!file.name) {
        error = new Error(`file[${i}].name missing`);
        break;
      }

      if (!file.data) {
        logger.warn(`file[${i}].data missing`);
        file.data = '';
        // error = new Error(`file[${i}].data missing`);
        // break;
      }

      if (file.base64) {
        file.data = new Buffer(file.base64, 'base64');
        file.base64 = undefined;
      }

      file.size = file.data.length;
      file.sha1 = checksum(file.data, 'sha1');
      file.sha256 = checksum(file.data, 'sha256');

      if (fileProvider === 'mongodb') {
        // use mongodb document
        logger.warn('store file %s to mongodb', file.name);
      } else if (fileProvider) {
        // store to filesystem
        filedb.storeFile(file)
          .then(() => {
            logger.silly(`File ${file.name} stored`);
          })
          .catch((storeError) => {
            logger.warn(storeError);
          });
        file.data = undefined;
      } else {
        // do not store at all..
        file.data = undefined;
        logger.warn('filedb is not configured');
      }
    }
  }

  if (error) {
    return next(error);
  }

  if (buildSha1) {
    logger.debug('result build sha1: ', buildSha1);
    Build.findOne({'files.sha1': buildSha1}, (findError, build) => {
      if (build) {
        this.exec.sut.ref = build._id;
      }
      next();
    });
  }

  return next(error);
});
ResultSchema.virtual('exec.sut.sha1');
/* .get()
.set(function(v) {

}); */
ResultSchema.methods.setBuild = function setBuild() {

};
ResultSchema.methods.getBuild = function getBuild(next) {
  logger.debug('lookup build..');
  Build.findOne({_id: _.get(this, 'exec.sut.ref')}, next);
};

/**
 * Statics
 */

ResultSchema.static({

});

/**
 * Register
 */
const Result = mongoose.model('Result', ResultSchema);
module.exports = {Model: Result, Collection: 'Result'};
<commit_msg>added skip option to exec verdict enum check. (#87)<commit_after>/*!
 * Module dependencies
 */

// Third party modules
const mongoose = require('mongoose');
// var userPlugin = require('mongoose-user');
const QueryPlugin = require('mongoose-query');
const logger = require('../tools/logger');
const _ = require('lodash');

// Local components
const FileSchema = require('./extends/file');
const tools = require('../tools');

const Schema = mongoose.Schema;
const checksum = tools.checksum;
const filedb = tools.filedb;
const fileProvider = filedb.provider;
const Build = mongoose.model('Build');

FileSchema.add({
  ref: {
    type: Schema.Types.ObjectId,
    ref: 'Resource'
  },
  from: {
    type: String,
    enum: ['dut', 'framework', 'env', 'other']
  }
});

/**
 * User schema
 */
const ResultSchema = new Schema({
  tcid: {type: String, required: true, index: true},
  tcRef: {type: Schema.Types.ObjectId, ref: 'Testcase'},
  job: {
    id: {type: String, default: ''}
  },
  campaign: {type: String, default: '', index: true},
  campaignRef: {type: Schema.Types.ObjectId, ref: 'Campaign'},
  cre: {
    time: {type: Date, default: Date.now, index: true},
    user: {type: String},
    userRef: {type: Schema.Types.ObjectId, ref: 'User'}
  },
  exec: {
    verdict: {
      type: String,
      required: true,
      enum: ['pass', 'fail', 'inconclusive', 'blocked', 'error', 'skip'],
      index: true
    },
    note: {type: String, default: ''},
    duration: {type: Number}, // seconds
    profiling: {type: Schema.Types.Mixed},
    env: { // environment information
      ref: {type: Schema.Types.ObjectId, ref: 'Resource'},
      rackId: {type: String},
      framework: {
        name: {type: String, default: ''},
        ver: {type: String, default: ''}
      }
    },
    sut: { // software under test
      ref: {type: Schema.Types.ObjectId, ref: 'Build'},
      gitUrl: {type: String, default: ''},
      buildName: {type: String},
      buildDate: {type: Date},
      buildUrl: {type: String, default: ''},
      buildSha1: {type: String},
      branch: {type: String, default: ''},
      commitId: {type: String, default: ''},
      tag: [{type: String}],
      href: {type: String},
      cut: [{type: String}], // Component Under Test
      fut: [{type: String}] // Feature Under Test
    },
    dut: { // Device(s) Under Test
      count: {type: Number},
      type: {type: String, enum: ['hw', 'simulator', 'process']},
      ref: {type: Schema.Types.ObjectId, ref: 'Resource'},
      vendor: {type: String},
      model: {type: String},
      ver: {type: String},
      sn: {type: String}
    },
    logs: [FileSchema]
  }
});

ResultSchema.set('toObject', {virtuals: true});

/**
 * Query plugin
 */
ResultSchema.plugin(QueryPlugin); // install QueryPlugin

/**
 * Add your
 * - pre-save hooks
 * - validations
 * - virtuals
 */

/**
 * Methods
 */
ResultSchema.pre('validate', function preValidate(next) {
  let error;
  const buildSha1 = _.get(this, 'exec.sut.buildSha1');

  const logs = _.get(this, 'exec.logs');
  if (Array.isArray(logs)) {
    for (let i = 0; i < logs.length; i += 1) {
      const file = logs[i];
      if (!file.name) {
        error = new Error(`file[${i}].name missing`);
        break;
      }

      if (!file.data) {
        logger.warn(`file[${i}].data missing`);
        file.data = '';
        // error = new Error(`file[${i}].data missing`);
        // break;
      }

      if (file.base64) {
        file.data = new Buffer(file.base64, 'base64');
        file.base64 = undefined;
      }

      file.size = file.data.length;
      file.sha1 = checksum(file.data, 'sha1');
      file.sha256 = checksum(file.data, 'sha256');

      if (fileProvider === 'mongodb') {
        // use mongodb document
        logger.warn('store file %s to mongodb', file.name);
      } else if (fileProvider) {
        // store to filesystem
        filedb.storeFile(file)
          .then(() => {
            logger.silly(`File ${file.name} stored`);
          })
          .catch((storeError) => {
            logger.warn(storeError);
          });
        file.data = undefined;
      } else {
        // do not store at all..
        file.data = undefined;
        logger.warn('filedb is not configured');
      }
    }
  }

  if (error) {
    return next(error);
  }

  if (buildSha1) {
    logger.debug('result build sha1: ', buildSha1);
    Build.findOne({'files.sha1': buildSha1}, (findError, build) => {
      if (build) {
        this.exec.sut.ref = build._id;
      }
      next();
    });
  }

  return next(error);
});
ResultSchema.virtual('exec.sut.sha1');
/* .get()
.set(function(v) {

}); */
ResultSchema.methods.setBuild = function setBuild() {

};
ResultSchema.methods.getBuild = function getBuild(next) {
  logger.debug('lookup build..');
  Build.findOne({_id: _.get(this, 'exec.sut.ref')}, next);
};

/**
 * Statics
 */

ResultSchema.static({

});

/**
 * Register
 */
const Result = mongoose.model('Result', ResultSchema);
module.exports = {Model: Result, Collection: 'Result'};
<|endoftext|>


<commit_before>import * as Promise from 'bluebird'
import * as Immutable from 'immutable'
import * as _ from 'lodash'

import uuid from 'app/helpers/uuid'
import randomChar from 'app/helpers/randomChar'
import countCredits from 'app/helpers/countCredits'

import Schedule from 'app/models/schedule'
import Study from 'app/models/study'

import * as demoStudent from 'sto-areas/demo_student.json'

let currentVersionString = '3.0.0alpha8'

let StudentRecord = Immutable.Record({
	id: uuid(),
	name: 'Student ' + randomChar(),
	active: false,
	version: currentVersionString,

	creditsNeeded: 35,

	matriculation: 1894,
	graduation: 1898,

	studies: Immutable.Map(),
	schedules: Immutable.Map(),
	overrides: Immutable.Map(),
	fabrications: Immutable.Map(),

	settings: Immutable.Map(),
})

class Student extends StudentRecord {
	constructor(encodedStudent={}) {
		// Don't pass the list params into the StudentRecord constructor; it creates them as JS objects,
		// instead of our custom Studies, Schedules, and such.
		let toRemove = Immutable.Set(['studies', 'schedules', 'overrides', 'fabrications'])
		let filtered = Immutable.fromJS(encodedStudent).filterNot((val, key) => toRemove.has(key))

		super(filtered)

		// Then add them manually.
		if (encodedStudent) {
			return this.withMutations((student) => {
				Immutable.Seq(encodedStudent.studies || [])
					.forEach(study => { student = student.addArea(study) })

				Immutable.Seq(encodedStudent.schedules || [])
					.forEach(schedule => { student = student.addSchedule(schedule) })

				Immutable.Seq(encodedStudent.overrides || [])
					.forEach(override => { student = student.addOverride(override) })

				Immutable.Seq(encodedStudent.fabrications || [])
					.forEach(fabrication => { student = student.addFabrication(fabrication) })

				return student
			})
		}
	}

	rename(newName) {
		return this.set('name', newName)
	}

	changeActive(isActive) {
		return this.set('active', isActive)
	}

	changeCreditsNeeded(newCreditsNeeded) {
		return this.set('creditsNeeded', newCreditsNeeded)
	}

	changeMatriculation(newMatriculation) {
		return this.set('matriculation', newMatriculation)
	}

	changeGraduation(newGraduation) {
		return this.set('graduation', newGraduation)
	}

	changeSetting(key, value) {
		return this.setIn(['settings', key], value)
	}

	// schedule methods

	get schedulesByYear() {
		return this.schedules.groupBy(sched => sched.year)
	}

	get activeSchedules() {
		return this.schedules.filter(sched => sched.active).sortBy(sched => sched.semester)
	}

	addSchedule(newSchedule) {
		let sched = new Schedule(newSchedule)
		return this.setIn(['schedules', sched.id], sched)
	}

	destroySchedule(scheduleId) {
		console.log(`removing schedule ${scheduleId}`)

		let deadSched = this.getIn(['schedules', scheduleId])
		let scheduleIsNoMore = this.set('schedules', this.schedules.delete(scheduleId))

		if (deadSched && deadSched.active) {
			let otherSchedKey = this.schedules.findKey((sched) =>
				sched.year === deadSched.year && sched.semester === deadSched.semester)

			if (otherSchedKey) {
				return scheduleIsNoMore.setIn(['schedules', otherSchedKey, 'active'], true)
			}
		}
		return scheduleIsNoMore
	}

	destroyMultipleSchedules(ids) {
		return this.withMutations((student) => {
			console.groupCollapsed('destroyMultipleSchedules')
			Immutable.Seq(ids).forEach((id) => {
				console.log('destroyMultipleSchedules', id)
				student = student.destroySchedule(id)
			})
			console.groupEnd('destroyMultipleSchedules')
			return student
		})
	}

	// area-of-study methods

	get areasByType() {
		return this.studies.groupBy((study) => study.type)
	}

	addArea(areaOfStudy) {
		let study = new Study(areaOfStudy)
		return this.setIn(['studies', study.id], study)
	}

	removeArea(id) {
		let removedIndex = this.studies.findIndex((study) => study.id === id)
		return this.set('studies', this.studies.delete(removedIndex))
	}

	removeMultipleAreas(ids) {
		Seq(ids).forEach(this.removeArea, this)
	}

	// misc

	addOverride(override) {
		return this
	}

	addFabrication(fabrication) {
		return this
	}

	// getters

	get courses() {
		let allCourses = this.activeSchedules.map((schedule) => schedule.courses).toArray()
		let scheduleCoursePromises = Promise.all(allCourses)
		return scheduleCoursePromises.then((results) => _.flatten(results))
	}

	get creditCount() {
		return this.courses.then(countCredits)
	}

	// helpers

	data() {
		return Promise.props({
			courses: this.courses,
			fabrications: this.fabrications.toArray(),
			overrides: this.overrides.toArray(),
			studies: this.studies.toArray(),
			creditsNeeded: this.creditsNeeded,
			graduation: this.graduation,
			matriculation: this.matriculation,
		})
	}

	encode() {
		return encodeURIComponent(this)
	}

	toString() {
		return JSON.stringify(this.toJS())
	}

	toJSON() {
		return JSON.stringify(this.toJS())
	}

	save() {
		console.log('saving student', this.name, '(' + this.id + ')')
		localStorage.setItem(this.id, this.toJSON())

		if (this.active)
			localStorage.setItem('activeStudentId', this.id)
	}
}

export default Student
<commit_msg>Student.encode should encode the JS version<commit_after>import * as Promise from 'bluebird'
import * as Immutable from 'immutable'
import * as _ from 'lodash'

import uuid from 'app/helpers/uuid'
import randomChar from 'app/helpers/randomChar'
import countCredits from 'app/helpers/countCredits'

import Schedule from 'app/models/schedule'
import Study from 'app/models/study'

import * as demoStudent from 'sto-areas/demo_student.json'

let currentVersionString = '3.0.0alpha8'

let StudentRecord = Immutable.Record({
	id: uuid(),
	name: 'Student ' + randomChar(),
	active: false,
	version: currentVersionString,

	creditsNeeded: 35,

	matriculation: 1894,
	graduation: 1898,

	studies: Immutable.Map(),
	schedules: Immutable.Map(),
	overrides: Immutable.Map(),
	fabrications: Immutable.Map(),

	settings: Immutable.Map(),
})

class Student extends StudentRecord {
	constructor(encodedStudent={}) {
		// Don't pass the list params into the StudentRecord constructor; it creates them as JS objects,
		// instead of our custom Studies, Schedules, and such.
		let toRemove = Immutable.Set(['studies', 'schedules', 'overrides', 'fabrications'])
		let filtered = Immutable.fromJS(encodedStudent).filterNot((val, key) => toRemove.has(key))

		super(filtered)

		// Then add them manually.
		if (encodedStudent) {
			return this.withMutations((student) => {
				Immutable.Seq(encodedStudent.studies || [])
					.forEach(study => { student = student.addArea(study) })

				Immutable.Seq(encodedStudent.schedules || [])
					.forEach(schedule => { student = student.addSchedule(schedule) })

				Immutable.Seq(encodedStudent.overrides || [])
					.forEach(override => { student = student.addOverride(override) })

				Immutable.Seq(encodedStudent.fabrications || [])
					.forEach(fabrication => { student = student.addFabrication(fabrication) })

				return student
			})
		}
	}

	rename(newName) {
		return this.set('name', newName)
	}

	changeActive(isActive) {
		return this.set('active', isActive)
	}

	changeCreditsNeeded(newCreditsNeeded) {
		return this.set('creditsNeeded', newCreditsNeeded)
	}

	changeMatriculation(newMatriculation) {
		return this.set('matriculation', newMatriculation)
	}

	changeGraduation(newGraduation) {
		return this.set('graduation', newGraduation)
	}

	changeSetting(key, value) {
		return this.setIn(['settings', key], value)
	}

	// schedule methods

	get schedulesByYear() {
		return this.schedules.groupBy(sched => sched.year)
	}

	get activeSchedules() {
		return this.schedules.filter(sched => sched.active).sortBy(sched => sched.semester)
	}

	addSchedule(newSchedule) {
		let sched = new Schedule(newSchedule)
		return this.setIn(['schedules', sched.id], sched)
	}

	destroySchedule(scheduleId) {
		console.log(`removing schedule ${scheduleId}`)

		let deadSched = this.getIn(['schedules', scheduleId])
		let scheduleIsNoMore = this.set('schedules', this.schedules.delete(scheduleId))

		if (deadSched && deadSched.active) {
			let otherSchedKey = this.schedules.findKey((sched) =>
				sched.year === deadSched.year && sched.semester === deadSched.semester)

			if (otherSchedKey) {
				return scheduleIsNoMore.setIn(['schedules', otherSchedKey, 'active'], true)
			}
		}
		return scheduleIsNoMore
	}

	destroyMultipleSchedules(ids) {
		return this.withMutations((student) => {
			console.groupCollapsed('destroyMultipleSchedules')
			Immutable.Seq(ids).forEach((id) => {
				console.log('destroyMultipleSchedules', id)
				student = student.destroySchedule(id)
			})
			console.groupEnd('destroyMultipleSchedules')
			return student
		})
	}

	// area-of-study methods

	get areasByType() {
		return this.studies.groupBy((study) => study.type)
	}

	addArea(areaOfStudy) {
		let study = new Study(areaOfStudy)
		return this.setIn(['studies', study.id], study)
	}

	removeArea(id) {
		return this.set('studies', this.studies.delete(id))
	}

	removeMultipleAreas(ids) {
		Seq(ids).forEach(this.removeArea, this)
	}

	// override methods

	addOverride(override) {
		return this.setIn(['overrides', override.id], override)
	}

	removeOverride(overrideId) {
		return this.set('overrides', this.overrides.delete(overrideId))
	}

	// fabrication methods

	addFabrication(fabrication) {
		return this.setIn(['fabrications', fabrication.id], fabrication)
	}

	removeFabrication(fabricationId) {
		return this.set('fabrications', this.fabrications.delete(fabricationId))
	}

	// getters

	get courses() {
		let allCourses = this.activeSchedules.map((schedule) => schedule.courses).toArray()
		let scheduleCoursePromises = Promise.all(allCourses)
		return scheduleCoursePromises.then((results) => _.flatten(results))
	}

	get creditCount() {
		return this.courses.then(countCredits)
	}

	// helpers

	data() {
		return Promise.props({
			courses: this.courses,
			fabrications: this.fabrications.toArray(),
			overrides: this.overrides.toArray(),
			studies: this.studies.toArray(),
			creditsNeeded: this.creditsNeeded,
			graduation: this.graduation,
			matriculation: this.matriculation,
		})
	}

	encode() {
		return encodeURIComponent(this.toJS())
	}

	toString() {
		return JSON.stringify(this.toJS())
	}

	toJSON() {
		return JSON.stringify(this.toJS())
	}

	save() {
		console.log(`saving student ${this.name} (${this.id})`)
		localStorage.setItem(this.id, this.toJSON())

		if (this.active)
			localStorage.setItem('activeStudentId', this.id)
	}
}

export default Student
<|endoftext|>


<commit_before>import chai from 'chai'
import {expect} from 'chai'
import chaiAsPromised from 'chai-as-promised'
import {Selector} from 'testcafe'
import demo from '../../test/e2e/demo/demo.po.js'

chai.use(chaiAsPromised)

fixture `type radio`
  .page(demo.url)

  test('value is a undefined by default', async page => {
    const {radioList} = demo.radio

    await expect(radioList.value).to.eventually.be.undefined
    await expect(radioList.hasClass('checked')).to.eventually.be.false
  })

  test('change value by click', async page => {
    const {optionStark, optionLannister} = demo.radio

    await page.click(optionStark.find('label'))
    await expect(optionStark.value).to.eventually.be.equal('stark')
    await expect(optionStark.hasClass('checked')).to.eventually.be.true
    await expect(optionLannister.hasClass('checked')).to.eventually.be.false

    await page.click(optionLannister.find('label'))
    await expect(optionStark.value).to.eventually.be.equal('lannister')
    await expect(optionStark.hasClass('checked')).to.eventually.be.false
    await expect(optionLannister.hasClass('checked')).to.eventually.be.true
  })

fixture `type checkbox`
  .page(demo.url)

  test('value is an array by default', async () => {
    const {checkboxList} = demo.checkbox

    await expect(checkboxList.value).to.eventually.be.an('array')
    await expect(checkboxList.value).to.eventually.be.empty
  })

  test('change value by click', async page => {
    const {optionNed, optionDaenerys} = demo.checkbox

    await page.click(optionNed.find('label'))
    await expect(optionNed.value).to.eventually.deep.equal(['ned'])
    await expect(optionNed.hasClass('checked')).to.eventually.be.true
    await expect(optionDaenerys.hasClass('checked')).to.eventually.be.false

    await page.click(optionDaenerys.find('label'))
    await expect(optionNed.value).to.eventually.deep.equal(['ned', 'daenerys'])
    await expect(optionNed.hasClass('checked')).to.eventually.be.true
    await expect(optionDaenerys.hasClass('checked')).to.eventually.be.true
  })

<commit_msg>better write with eventually<commit_after>import chai from 'chai'
import {expect} from 'chai'
import chaiAsPromised from 'chai-as-promised'
import {Selector} from 'testcafe'
import demo from '../../test/e2e/demo/demo.po.js'

chai.use(chaiAsPromised)

fixture `type radio`
  .page(demo.url)

  test('value is a undefined by default', async page => {
    const {radioList} = demo.radio

    await expect(radioList.value).to.be.eventually.undefined
    await expect(radioList.hasClass('checked')).to.be.eventually.false
  })

  test('change value by click', async page => {
    const {optionStark, optionLannister} = demo.radio

    await page.click(optionStark.find('label'))
    await expect(optionStark.value).to.be.eventually.equal('stark')
    await expect(optionStark.hasClass('checked')).to.be.eventually.true
    await expect(optionLannister.hasClass('checked')).to.be.eventually.false

    await page.click(optionLannister.find('label'))
    await expect(optionStark.value).to.be.eventually.equal('lannister')
    await expect(optionStark.hasClass('checked')).to.be.eventually.false
    await expect(optionLannister.hasClass('checked')).to.be.eventually.true
  })

fixture `type checkbox`
  .page(demo.url)

  test('value is an array by default', async () => {
    const {checkboxList} = demo.checkbox

    await expect(checkboxList.value).to.be.eventually.an('array')
    await expect(checkboxList.value).to.be.eventually.empty
  })

  test('change value by click', async page => {
    const {optionNed, optionDaenerys} = demo.checkbox

    await page.click(optionNed.find('label'))
    await expect(optionNed.value).to.deep.eventually.equal(['ned'])
    await expect(optionNed.hasClass('checked')).to.be.eventually.true
    await expect(optionDaenerys.hasClass('checked')).to.be.eventually.false

    await page.click(optionDaenerys.find('label'))
    await expect(optionNed.value).to.deep.eventually.equal(['ned', 'daenerys'])
    await expect(optionNed.hasClass('checked')).to.be.eventually.true
    await expect(optionDaenerys.hasClass('checked')).to.be.eventually.true
  })

<|endoftext|>


<commit_before>if (!String.prototype.trim) {
    String.prototype.trim = function () {
        return this.replace(/^\s+|\s+$/g, '');
    };
}
/******************************************************************************/
/********************************* Atto class *********************************/
/******************************************************************************/
var Atto = (function AttoClosure() {
    'use strict';
    var attoMap = {},
        emptyArray = [],
        slice = emptyArray.slice,
        atto,
        constants = {
            ATTO_SELECTOR_CLASS_PREFIX: 'js-atto-selector-'
        },
        AttoNotification;

    function AttoException(message) {
        this.message = message;
        this.name = 'methodNotDefined';
        this.toString = function () {
            return this.message;
        };
    }

    /**************************************************************************/
    /***************************** private method *****************************/
    /**************************************************************************/
    function extend(obj1, obj2) {
        var returnObject = {},
            prop;
        for (prop in obj2) {
            if (obj2.hasOwnProperty(prop)) {
                returnObject[prop] = obj2[prop];
            }
        }
        for (prop in obj1) {
            if (obj1.hasOwnProperty(prop)) {
                returnObject[prop] = obj1[prop];
            }
        }
        return returnObject;
    }
    function renderAttoElement(render, renderInSection) {
        var wrapper, len, i;
        if (renderInSection) {
            wrapper = document.createElement('section');
        } else {
            wrapper = document.createDocumentFragment();
            wrapper.isFragment = true;
        }
        if (toString.call(render) === '[object Array]') {
            len = render.length;
            for (i = 0; i < len; i++) {
                wrapper.appendChild(render[i]);
            }
        } else if (typeof render === 'string') {
            wrapper.innerHTML = render.trim();
        } else {
            wrapper.appendChild(render);
        }
        return wrapper;
    }
    function renderIntoElement(element) {
        var map = attoMap, elements,
            newElement = element.cloneNode(true),
            newElement1 = document.createDocumentFragment(),
            attributes = newElement.attributes,
            i, len, name;

        newElement.innerHTML = element.innerHTML;
        element.innerHTML = '';

        for (name in map) {
            if (map.hasOwnProperty(name)) {
                elements = newElement.getElementsByTagName(map[name].name);
                while (0 < elements.length) {
                    new Atto({
                        target: elements[0],
                        name: name,
                        renderInSection: true
                    }).render(true);
                }
            }
        }

        element.parentNode.replaceChild(newElement, element);
    }

    /**************************************************************************/
    /****************************** constructor *******************************/
    /**************************************************************************/
    function Atto(option) {
        //target, name, configObject, thisObj, customAttributes, renderInSection
        var config,
            len,
            i,
            props = {},
            attributes,
            target,
            customAttributes;

        config = option.config || attoMap[option.name] || {};
        target = option.target || [];
        customAttributes = option.ttributes || {};
        if (!config.isExtened && config.hasOwnProperty('extend')) {
            this.parent = attoMap[config.extend];
            this.config = extend(config, this.parent);
        } else {
            this.config = config;
        }
        if (target.hasOwnProperty('attributes')) {
            attributes = target.attributes;
            len = attributes.length;
            for (i = 0; i < len; i++) {
                props[attributes[i].name] = attributes[i].value;
            }
        }
        this.mergeWith(this.config);

        this.mergeWith.call(props, customAttributes);
        this.props = props;
        this.target = target;
        this.class = config.class;
        this.id = config.id;
        this.renderInSection = option.renderInSection;
    }

    /**************************************************************************/
    /***************************** public method ******************************/
    /**************************************************************************/
    Atto.prototype = {
        AttoFromConfig: function Atto_renderFromConfig(config, thisObj) {
        },
        render: function Atto_render(inDom) {
            var renderer = this.config.render.apply(this, [this.config, this]),
                attributes = this.attributes,
                activeElement,
                element = renderAttoElement(renderer, this.renderInSection),
                att;
            if (typeof element === 'undefined') {
                throw new AttoException(
                    'Render method must be implemented and should return value.'
                );
            // render must be implement and return value
            } else if (element.isFragment) {
                activeElement = element.childNodes[0];
            } else {
                activeElement = element;
                for (att in attributes) {
                    if (attributes.hasOwnProperty(att)) {
                        activeElement.setAttribute(att, attributes[att]);
                    }
                }
            }
            activeElement.settingAtto = this.config;
            activeElement.className += ' ' + (this.class || '') +
                (this.config.name ?
                        ' ' +
                        constants.ATTO_SELECTOR_CLASS_PREFIX + this.config.name
                        : '');
            if (this.id) {
                activeElement.id = this.id;
            }
            this.element = element;
            // put new element in dom
            if (inDom) {
                this.insert();
            }
            return activeElement;
        },

        insert: function Atto_insert() {
            this.target.parentNode.replaceChild(this.element, this.target);
        },
        setAttribute: function Atto_setattribute(name, value) {
            this.attributes[name] = value;
        },
        removeAttribute: function Atto_removeAttribute(name) {
            if (this.attributes.hasOwnProperty(name)) {
                delete this.attributes[name];
            }
        },
        get: function Atto_get(name) {
            if (attoMap.hasOwnProperty(name)) {
                return new Atto({
                    name: name
                });
            }
            return null;
        },
        observer: function Atto_event(name, callBack) {
            AttoNotification.observer(name, callBack, this);
        },
        post: function Atto_post(name, mixin) {
            AttoNotification.post(name, mixin);
        },
        rerender: function Atto_rerender() {
            this.target = this.element;
            this.render(true);
        },
        mergeWith: function Atto_mergeWith(object) {
            var prop;
            for (prop in object) {
                if (object.hasOwnProperty(prop) && prop !== 'render') {
                    this[prop] = object[prop];
                }
            }
        }
    };

    /**************************************************************************/
    /***************************** static method ******************************/
    /**************************************************************************/
    Atto.define = function Atto_define(config) {
        if (config.hasOwnProperty('name')) {
            if (config.hasOwnProperty('extend')) {
                var ext = attoMap[config.extend];
                if (!ext) {
                    throw new AttoException(
                        'Cannot extend, unknown component "' + config.extend +
                            '".'
                    );
                }
                attoMap[config.name] = extend(config, ext);
                attoMap[config.name].isExtend = true;
            } else {
                attoMap[config.name] = config;
            }
        } else {
            throw new AttoException(
                'Name configuration property must be defined.'
            );
        }
    };
    Atto.run = function Atto_run(settings) {
        var elements,
            len,
            i;

        settings = settings || {};

        if (settings.hasOwnProperty('target') &&
                (elements = settings.target) &&
                typeof elements.length === 'number') {
            len = elements.length;
            for (i = 0; i < len; i++) {
                renderIntoElement(elements[i]);
            }
        } else {
            renderIntoElement(document.body);
        }
        this.settings = settings;
    };
    Atto.create = function Atto_create(config) {
        if (typeof config === 'object') {
            return new Atto({
                config: config,
                renderInSection: false
            });
        }
        // throw error
        return null;
    };

    Atto.find = function Atto_find(name, scope) {
        var elements,
            list = [],
            len,
            i,
            el;
        scope = scope || document;
        if (attoMap.hasOwnProperty(name)) {
            elements = scope.getElementsByClassName(
                constants.ATTO_SELECTOR_CLASS_PREFIX + name
            );
            len = elements.length;
            for (i = 0; i < len; i++) {
                el = elements[i];
                list.push(new Atto({target: el,
                    name: el.settingAtto.name,
                    config: el.settingAtto
                    }));
            }

            return list;
        }

        return null;
    };

    Atto.get = function Atto_get(name, attributes) {
        if (attoMap.hasOwnProperty(name)) {
            return new Atto({
                name: name,
                config: attoMap[name],
                attributes: attributes,
                renderInSection: false
            });
        }
        // throw error
        return null;
    };

    AttoNotification = (function AttoNotificationClosure() {
        var notification = {};
        function AttoNotification(notificationName, callBack, id, context) {
            this.name = notificationName;
            this.callBack = callBack || function () {};
            this.id = id;
            this.target = context;
        }

        AttoNotification.prototype = {
            execute: function AttoNotification_execute(mixin) {
                this.callBack.apply(this.target, [mixin, this]);
            }
        };

        AttoNotification.observer = function AttoNotification_observer(name, callBack, context) {
            var notif, id;

            if (!notification.hasOwnProperty(name)) {
                notification[name] = [];
            }

            if (!context.hasOwnProperty('notifications')) {
                context.notifications = {};
            }

            notif = notification[name];
            id = notif.length;

            if (typeof context.notifications[name] === 'undefined') {
                notif.push(new AttoNotification(name, callBack, id, context));
                context.notifications[name] = id;
            }
        };
        AttoNotification.post = function AttoNotification_post(name, mixin) {
            var list,
                len,
                i = 0,
                interval;
            if (notification.hasOwnProperty(name)) {
                list = notification[name];
            }
            len = list.length;
            interval = setInterval(function () {
                if (i > len) {
                    clearInterval(interval);
                }
                if (typeof list[i] !== 'undefined') {
                    list[i].execute(mixin);
                } else {
                    clearInterval(interval);
                }
                i++;
            }, 10);
        };

        return AttoNotification;
    }());

    return Atto;
}());
<commit_msg>refactoring<commit_after>if (!String.prototype.trim) {
    String.prototype.trim = function () {
        return this.replace(/^\s+|\s+$/g, '');
    };
}
/******************************************************************************/
/********************************* Atto class *********************************/
/******************************************************************************/
var Atto = (function () {
    'use strict';
    var attoMap = {},
        emptyArray = [],
        slice = emptyArray.slice,
        atto,
        toString = Object.prototype.toString,
        constants = {
            ATTO_SELECTOR_CLASS_PREFIX: 'js-atto-selector-'
        };

    function AttoException(message) {
        this.message = message;
        this.name = 'methodNotDefined';
        this.toString = function () {
            return this.message;
        };
    }

    /**************************************************************************/
    /***************************** private method *****************************/
    /**************************************************************************/
    function extend(obj1, obj2) {
        var returnObject = {};
        for (var prop in obj2) {
            if (obj2.hasOwnProperty(prop)) {
                returnObject[prop] = obj2[prop];
            }
        }
        for (var prop in obj1) {
            if (obj1.hasOwnProperty(prop)) {
                returnObject[prop] = obj1[prop];
            }
        }
        return returnObject;
    }
    function renderAttoElement(render, renderInSection) {
        var wrapper;
        if (renderInSection) {
            wrapper = document.createElement('section')
        } else {
            wrapper = document.createDocumentFragment();
            wrapper.isFragment = true;
        }
        if (toString.call(render) == '[object Array]') {
            var len, i;
            len = render.length;
            for (i = 0; i < len; i++) {
                wrapper.appendChild(render[i]);
            }
        } else if (typeof render === 'string') {
            wrapper.innerHTML = render.trim();
        } else {
            wrapper.appendChild(render);
        }
        return wrapper;
    }
    function renderIntoElement(element) {
        var map = attoMap, elements,
            newElement = element.cloneNode(true),
            newElement1 = document.createDocumentFragment(),
            attributes = newElement.attributes,
            i, len;

        newElement.innerHTML = element.innerHTML;
        element.innerHTML = '';

        for (var name in map) {
            if (map.hasOwnProperty(name)) {
                elements = newElement.getElementsByTagName(map[name].name);
                while (0 < elements.length) {
                    new Atto({
                        target: elements[0],
                        name: name,
                        renderInSection: true
                    }).render(true);
                }
            }
        }

        element.parentNode.replaceChild(newElement, element);
    };

    /**************************************************************************/
    /****************************** constructor *******************************/
    /**************************************************************************/
    function Atto(option) {
        //target, name, configObject, thisObj, customAttributes, renderInSection
        var config,
            len, i,
            props = {}, attributes, target, customAttributes;

        config = option.config || attoMap[option.name] || {};
        target = option.target || [];
        customAttributes = option.attributes || {};
        if (!config.isExtened && config.hasOwnProperty('extend')) {
            this.parent = attoMap[config.extend];
            this.config = extend(config, this.parent);
        } else {
            this.config = config;
        }
        if (target.hasOwnProperty('attributes')) {
            attributes = target.attributes;
            len = attributes.length;
            for (i = 0; i < len; i++) {
                props[attributes[i].name] = attributes[i].value;
            }
        }
        this.mergeWith(this.config);

        this.mergeWith.call(props, customAttributes);
        this.props = props;
        this.target = target
        this.class = config.class;
        this.id = config.id;
        this.renderInSection = option.renderInSection;
    };

    /**************************************************************************/
    /***************************** public method ******************************/
    /**************************************************************************/
    Atto.prototype = {
        AttoFromConfig: function Atto_renderFromConfig(config, thisObj) {
        },
        render: function Atto_render(inDom) {
            var renderer = this.config.render.apply(this, [this.config, this]),
                attributes = this.attributes,
                activeElement,
                element = renderAttoElement(renderer, this.renderInSection);
            if (typeof element === 'undefined') {
                throw new AttoException(
                    'Render method must be implemented and should return value.'
                );
            // render must be implement and return value
            } else if (element.isFragment) {
                activeElement = element.childNodes[0];
            } else {
                activeElement = element;
                for (var att in attributes) {
                    if (attributes.hasOwnProperty(att)) {
                        activeElement.setAttribute(att, attributes[att]);
                    }
                }
            }
            activeElement.settingAtto = this.config;
            activeElement.className += ' ' + (this.class || '') +
            (this.config.name ?
                 ' ' +
                 constants.ATTO_SELECTOR_CLASS_PREFIX + this.config.name
                 : '');
            if (this.id) {
                activeElement.id = this.id;
            }
            this.element = element;
            // put new element in dom
            if (inDom) {
                this.insert();
            }
            return activeElement;
        },

        insert: function Atto_insert() {
            this.target.parentNode.replaceChild(this.element, this.target);
        },
        setAttribute: function Atto_setattribute(name, value) {
            this.attributes[name] = value;
        },
        removeAttribute: function Atto_removeAttribute(name) {
            if (this.attributes.hasOwnProperty(name)) {
                delete this.attributes[name];
            }
        },
        get: function Atto_get(name) {
            if (attoMap.hasOwnProperty(name)) {
                return new Atto({
                    name: name
                });
            }
            return null;
        },
        observer: function Atto_event(name, callBack) {
            AttoNotification.observer(name, callBack, this);
        },
        post: function Atto_post(name, mixin) {
            AttoNotification.post(name, mixin);
        },
        rerender: function Atto_rerender() {
            // this.target = this.element;
            this.render(true);
        },
        mergeWith: function Atto_mergeWith(object) {
            for (var prop in object) {
                if (object.hasOwnProperty(prop) && prop !== 'render') {
                    this[prop] = object[prop];
                }
            }
        }
    };

    /**************************************************************************/
    /***************************** static method ******************************/
    /**************************************************************************/
    Atto.define = function Atto_define(config) {
        if (config.hasOwnProperty('name')) {
           if (config.hasOwnProperty('extend')) {
               var ext = attoMap[config.extend];
               if (!ext) {
                   throw new AttoException(
                       'Cannot extend, unknown component "' + config.extend +
                       '".'
                   );
               }
               attoMap[config.name] = extend(config, ext);
               attoMap[config.name].isExtend = true;
           } else {
               attoMap[config.name] = config;
           }
        } else {
           throw new AttoException(
               'Name configuration property must be defined.'
           );
        }
    };
    Atto.run = function Atto_run(settings) {
        var elements, len, i;

        settings = settings || {};

        if (settings.hasOwnProperty('target') &&
            (elements = settings.target) &&
            typeof elements.length === 'number') {
            len = elements.length;
            for (i = 0; i < len; i++) {
                renderIntoElement(elements[i]);
            }
        } else {
            renderIntoElement(document.body);
        }
        this.settings = settings;
    };
    Atto.create = function Atto_create(config) {
        if (typeof config === 'object') {
            return new Atto({
                config: config,
                renderInSection: false
            });
        }
        // throw error
        return null;
    };

    Atto.find = function Atto_find(name, scope) {
        var elements,
            list = [],
            len, i, el;
        scope = scope || document;
        if (attoMap.hasOwnProperty(name)) {
            elements = scope.getElementsByClassName(
                constants.ATTO_SELECTOR_CLASS_PREFIX + name
            );
            len = elements.length;
            for (i = 0; i < len; i++) {
                el = elements[i];
                list.push(new Atto({
                    target: el,
                    name: el.settingAtto.name,
                    config: el.settingAtto,
                    renderInSection: true
                }));
            }

            return list;
        }

        return null;
    };

    Atto.get = function Atto_get(name, attributes) {
        if (attoMap.hasOwnProperty(name)) {
            return new Atto({
                name: name,
                config: attoMap[name],
                attributes: attributes,
                renderInSection: false
            });
        }
        // throw error
        return null;
    };

    var AttoNotification = (function AttoNotificationClosure() {
        'use strict';
        var notification = {};
        function AttoNotification(notificationName, callBack, id, context) {
            this.name = notificationName;
            this.callBack = callBack || function () {};
            this.id = id;
            this.target = context;
        }

        AttoNotification.prototype = {
            execute: function AttoNotification_execute(mixin) {
                this.callBack.apply(this.target, [mixin, this]);
            }
        };

        AttoNotification.observer = function AttoNotification_observer(name, callBack, context) {
            var notif, id;

            if (!notification.hasOwnProperty(name)) {
                notification[name] = [];
            }

            if (!context.hasOwnProperty('notifications')) {
                context.notifications = {};
            }

            notif = notification[name];
            id = notif.length;

            if (typeof context.notifications[name] === 'undefined') {
                notif.push(new AttoNotification(name, callBack, id, context));
                context.notifications[name] = id;
            }
        }
        AttoNotification.post = function AttoNotification_post(name, mixin) {
            var list, len, i = 0;
            if (notification.hasOwnProperty(name)) {
                list = notification[name];
            }
            len = list.length;
            var interval = setInterval(function () {
                if (i > len) {
                    clearInterval(interval);
                }
                if (typeof list[i] !== 'undefined') {
                    list[i].execute(mixin);
                } else {
                    clearInterval(interval);
                }
                i++;
            }, 10);
        };

        return AttoNotification;
    }());

    return Atto;
}());

<|endoftext|>


<commit_before>/** ****************************************************************************
*   @license                                                                   *
*   Copyright 2017 random-parts. All Rights Reserved.                          *
*                                                                              *
*   Licensed under the Apache License, Version 2.0 (the "License");            *
*   you may not use this file except in compliance with the License.           *
*   You may obtain a copy of the License at                                    *
*                                                                              *
*       http://www.apache.org/licenses/LICENSE-2.0                             *
*                                                                              *
*   Unless required by applicable law or agreed to in writing, software        *
*   distributed under the License is distributed on an "AS IS" BASIS,          *
*   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   *
*   See the License for the specific language governing permissions and        *
*   limitations under the License.                                             *
***************************************************************************** */

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*       _____               ___               _      ___  _____   _____        *
*      |_   _|__ __ _ _ __ / __|_ __  ___ _ _| |_ __| _ \/ __\ \ / / _ \       *
*        | |/ -_) _` | '  \\__ \ '_ \/ _ \ '_|  _(_-<   /\__ \\ V /|  _/       *
*        |_|\___\__,_|_|_|_|___/ .__/\___/_|  \__/__/_|_\|___/ \_/ |_|         *
*                              |_|                                             *
*                                                                              *
*        Team Sports RSVP - Team schedule management spreadsheet add-on        *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

/**
 * @file Contains the controller methods used to orchestrate script actions
 * @license Apache License, Version 2.0
 * @version 1.3.18
 */

/**
 * ---
 * Script Configuration Object for debugging. Sets the proper Spreadsheeet Object
 * depending on if the script is run as an add-on[!debug]
 * or from the script-editor[debug]
 *
 * @namespace Config
 * @property {Boolean} debug
 * @property {String} debug_ss_id - spreadsheet id for testing
 * @property {Spreadsheet} spreadsheet - `debug:` SpreadsheetByID()

 * `!debug:` ActiveSpreadsheet()
 * @property {Form} team_form - `debug:` openByUrl()
 `!debug:` getActiveForm()
 */
var Config = {
  version: "1.3.18",
  debug: false,
  debug_ss_id: "SHEET_ID",
  spreadsheet: function () {
    if (this.debug) { return SpreadsheetApp.openById(this.debug_ss_id) }
     else { return SpreadsheetApp.getActiveSpreadsheet() }
  },
  storage: function () {
    if (this.debug) { return PropertiesService.getScriptProperties() }
     else { return PropertiesService.getDocumentProperties() }
  },
  team_form: function () {
      var form_url = this.spreadsheet().getFormUrl();
      return (form_url != null) ? FormApp.openByUrl(form_url) : null;
  }
}

/**
 * ---
 * onInstall() event that runs once. Runs the first time the add-on is installed.
 * It sets-up the triggers then runs onOpen to build the menu.
 *
 * @param {EventObject} e
 */
function onInstall (e) {
  // Install the script project triggers
  onUpdateTriggers();

  // Run to get the Add-on Menu
  onOpen(e);
}

/**
 * ---
 * onOpen() event runs the start-up processes, i.e add-on menu build,
 * when the speadsheet is opened.
 *
 * @param {EventObject} e
 */
function onOpen (e) {
  var ss = Config.spreadsheet();

  // Check that the script is up-to-date
  utils(ss).script.update();

  // Build the Add-On Menu
  ui().menu(e);

  ss.setActiveSheet(ss.getSheets()[0]);
}

/**
 * ---
 * Auto runs the daily tasks. Set to update the schedule and then
 * process any emails that should go out for the day.
 */
function onDailyTrigger () {
  var ss = Config.spreadsheet();

  // Check that the script is up-to-date
  utils(ss).script.update();

  // Create a team form if one is not attached
  if (ss.getFormUrl() == null) { onCreateForm() }

  // Update the form confirmation message
  formService().updateConfirmation();

  // Update the displayed schedule
  scheduleService().update();

  // Run email task
  emailService().sendMail("today");

  // Send daily email log
  emailService().sendMail("log");

  // Clear the email log
  storage().clear("email");
}

/**
 * ---
 * Form submit event triggers the app to update the displayed season schedule
 *
 * @param {EventObject} e
 */
function onFormSubmit (e) {
  formService().handleResponse(e);
}

/**
 * ---
 * Sheet edit event within the web schedule range triggers an update to
 * the displayed schedule when it pull changes in the schedule from the website
 *
 * @requires Installed onEdit() Trigger
 * @param {EventObject} e
 */
function onSheetEdit (e) {
  var ss = e.source;
  var range = e.range;
  var change_cell_row = e.range.rowStart;
  var change_cell_col = e.range.columnStart;
  var data_range = ss.getRangeByName("webSchedule");

  // Check if the edits are with the web data/schedule values
  if ((data_range.getRow() <= change_cell_row == change_cell_row <= data_range.getLastRow()) &&
      (data_range.getColumn() <= change_cell_col == change_cell_col <= data_range.getLastColumn())) {
    // Run the schedule update
    scheduleService().update();
  }
}

/**
 * ---
 * Creates the sheet(s) needed to run a new season. Can be run from the
 * Add-on Menu.
 *
 */
function onNewSeason () {
  var ss = Config.spreadsheet();
  // Check that the script is up-to-date
  utils(ss).script.update();

  var sheet_name = ui().prompt.sheetname;
  var ss_triggers = ScriptApp.getUserTriggers(ss);
  var active_sheet;
   // Install add-on triggers for this spreadsheet
   if (ss_triggers.length == 0) { onUpdateTriggers() }
   // Create a template sheet if one does not exist
   if (template(ss).getTemplate() == null) { sheetService().create.template() }

  // Create a season sheet
  sheetService().create.season(sheet_name);
  // Give it time to do what it needs to do
  Utilities.sleep(250);

  // Set the new sheet as the ActiveSheet
  active_sheet = ss.getSheets()[0];
  active_sheet.showSheet();
  ss.setActiveSheet(active_sheet);

  SpreadsheetApp.flush();
  Utilities.sleep(250);

  // Copy the returning squad mates to the new season sheet
  teamService().squad.setReturning();

  Utilities.sleep(250);

  // Set the new web/online schedule link & team name
  onWeblinkUpdate();

  // Create a team form if one is not attached
  if (ss.getFormUrl() == null) { onCreateForm() }

  // Set the sheet edit protection & data validation
  sheetService().update.protection();
  sheetService().update.validation();

  // Let the user know the set-up is finished
  ui().alert.done();
}

/*******************************************************************************
*                       Menu Options - Advanced Features                       *
*******************************************************************************/

/**
 * --- Menu Option: "Extra Options Help" */
function onOptionsHelp () { ui().modal.optionsHelp() }

/**
 * --- Menu Option: "Re-install Triggers" */
function onUpdateTriggers () { utils().script.install.triggers() }

/**
 * --- Menu Option: "Remove Triggers" */
function onRemoveTriggers () { utils().script.clean.triggers() }

/**
 * --- Menu Option: "Remove Conditional Formatting" */
function onRemoveCondFormatting () { sheetService().clear.conditionalFormatting() }

/**
 * --- Menu Option: "Remove Dependencies [soft un-install]" */
function onRemoveDependencies () {
  utils().script.clean.triggers();
  settings().clearAll();
  formService().removeForm();

  SpreadsheetApp.flush();
  Utilities.sleep(500);

  sheetService().remove.sheet(settings().sheet.template_name);
}

/**
 * --- Menu Option: "Restore Data Validation" */
function onUpdateDataValidation () { sheetService().update.validation() }

/**
 * --- Menu Option: "Restore NamedRanges" */
function onUpdateNamedRange () { sheetService().update.namedRanges() }

/**
 * --- Menu Option: "Restore Conditional Formatting" */
function onUpdateCondFormatting () { sheetService().update.conditionalFormatting() }

/**
 * ---
 * Add-on Menu option to manually run the schedule update.
 */
function onRunSchedule () {
  scheduleService().update();
}

/**
 * ---
 * Add-on Menu option to manually send debt emails
 */
function onRunDebtEmail () {
  var ss = Config.spreadsheet();
  var isEmpty = ss.getRangeByName("squadEmail").isBlank();

  if (!isEmpty) {
     emailService().sendMail("other", "debt");
  }
}

/**
 * ---
 * Add-on Menu option to manually send emails for the next gameday
 */
function onRunNextEmail () {
  var ss = Config.spreadsheet();
  var isEmpty = ss.getRangeByName("squadEmail").isBlank();

  if (!isEmpty && ui().confirm.sendemail() == true) {
     emailService().sendMail("next");
  }
}

/**
 * ---
 * Add-on Menu option to manually send `returning next season` emails
 */
function onRunReturningEmail () {
  var ss = Config.spreadsheet();
  var isEmpty = ss.getRangeByName("squadEmail").isBlank();

  if (!isEmpty) {
     emailService().sendMail("other", "returning");
  }
}

/**
 * ---
 * Prompts the user to enter the link for the online schedule,
 * sets it in the sheet, and sets the team name in the spreadsheet and
 * within the sheet's attached Form from the resulting schedule.
 *
 */
function onWeblinkUpdate () {
  var name_prompt = ui().confirm.teamname;
  var isConfirmed = ui().confirm.clearall;

  // Prompt to clear schedule range if it is not blank
  if (!scheduleService().isScheduleBlank()) {
    if (!isConfirmed()) { return }

    sheetService().clear.scheduleRanges();
    // Reset data validation and protections
    sheetService().update.protection();
    sheetService().update.validation();
  }

  // Prompt and set the web link
  scheduleService().setWebLink(ui().prompt.weblink());
  teamService().updateName(name_prompt);

  SpreadsheetApp.flush();
  Utilities.sleep(500);

  // Update the form title to match the team name
  formService().updateTitle(team().name);

  // Update displayed schedule
  onRunSchedule();
}

/**
 * ---
 * Detaches the active form from the spreadsheet before
 * creating a new form and attaching it.
 */
function onResetForm () {
  // Detach the attached form and clear the response sheet
  formService().removeForm();

  SpreadsheetApp.flush();
  Utilities.sleep(500);
  // Create Team Form
  onCreateForm();
}

/**
 * ---
 * Creating a new form and attaches it to the spreadsheet
 */
 function onCreateForm () {
  // Create Team Form
  formService().createForm();

  SpreadsheetApp.flush();
  Utilities.sleep(500);
  // Update the form confirmation message
  formService().updateConfirmation();
}<commit_msg>increment version<commit_after>/** ****************************************************************************
*   @license                                                                   *
*   Copyright 2017 random-parts. All Rights Reserved.                          *
*                                                                              *
*   Licensed under the Apache License, Version 2.0 (the "License");            *
*   you may not use this file except in compliance with the License.           *
*   You may obtain a copy of the License at                                    *
*                                                                              *
*       http://www.apache.org/licenses/LICENSE-2.0                             *
*                                                                              *
*   Unless required by applicable law or agreed to in writing, software        *
*   distributed under the License is distributed on an "AS IS" BASIS,          *
*   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   *
*   See the License for the specific language governing permissions and        *
*   limitations under the License.                                             *
***************************************************************************** */

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*       _____               ___               _      ___  _____   _____        *
*      |_   _|__ __ _ _ __ / __|_ __  ___ _ _| |_ __| _ \/ __\ \ / / _ \       *
*        | |/ -_) _` | '  \\__ \ '_ \/ _ \ '_|  _(_-<   /\__ \\ V /|  _/       *
*        |_|\___\__,_|_|_|_|___/ .__/\___/_|  \__/__/_|_\|___/ \_/ |_|         *
*                              |_|                                             *
*                                                                              *
*        Team Sports RSVP - Team schedule management spreadsheet add-on        *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

/**
 * @file Contains the controller methods used to orchestrate script actions
 * @license Apache License, Version 2.0
 * @version 1.3.19
 */

/**
 * ---
 * Script Configuration Object for debugging. Sets the proper Spreadsheeet Object
 * depending on if the script is run as an add-on[!debug]
 * or from the script-editor[debug]
 *
 * @namespace Config
 * @property {Boolean} debug
 * @property {String} debug_ss_id - spreadsheet id for testing
 * @property {Spreadsheet} spreadsheet - `debug:` SpreadsheetByID()

 * `!debug:` ActiveSpreadsheet()
 * @property {Form} team_form - `debug:` openByUrl()
 `!debug:` getActiveForm()
 */
var Config = {
  version: "1.3.19",
  debug: false,
  debug_ss_id: "SHEET_ID",
  spreadsheet: function () {
    if (this.debug) { return SpreadsheetApp.openById(this.debug_ss_id) }
     else { return SpreadsheetApp.getActiveSpreadsheet() }
  },
  storage: function () {
    if (this.debug) { return PropertiesService.getScriptProperties() }
     else { return PropertiesService.getDocumentProperties() }
  },
  team_form: function () {
      var form_url = this.spreadsheet().getFormUrl();
      return (form_url != null) ? FormApp.openByUrl(form_url) : null;
  }
}

/**
 * ---
 * onInstall() event that runs once. Runs the first time the add-on is installed.
 * It sets-up the triggers then runs onOpen to build the menu.
 *
 * @param {EventObject} e
 */
function onInstall (e) {
  // Install the script project triggers
  onUpdateTriggers();

  // Run to get the Add-on Menu
  onOpen(e);
}

/**
 * ---
 * onOpen() event runs the start-up processes, i.e add-on menu build,
 * when the speadsheet is opened.
 *
 * @param {EventObject} e
 */
function onOpen (e) {
  var ss = Config.spreadsheet();

  // Check that the script is up-to-date
  utils(ss).script.update();

  // Build the Add-On Menu
  ui().menu(e);

  ss.setActiveSheet(ss.getSheets()[0]);
}

/**
 * ---
 * Auto runs the daily tasks. Set to update the schedule and then
 * process any emails that should go out for the day.
 */
function onDailyTrigger () {
  var ss = Config.spreadsheet();

  // Check that the script is up-to-date
  utils(ss).script.update();

  // Create a team form if one is not attached
  if (ss.getFormUrl() == null) { onCreateForm() }

  // Update the form confirmation message
  formService().updateConfirmation();

  // Update the displayed schedule
  scheduleService().update();

  // Run email task
  emailService().sendMail("today");

  // Send daily email log
  emailService().sendMail("log");

  // Clear the email log
  storage().clear("email");
}

/**
 * ---
 * Form submit event triggers the app to update the displayed season schedule
 *
 * @param {EventObject} e
 */
function onFormSubmit (e) {
  formService().handleResponse(e);
}

/**
 * ---
 * Sheet edit event within the web schedule range triggers an update to
 * the displayed schedule when it pull changes in the schedule from the website
 *
 * @requires Installed onEdit() Trigger
 * @param {EventObject} e
 */
function onSheetEdit (e) {
  var ss = e.source;
  var range = e.range;
  var change_cell_row = e.range.rowStart;
  var change_cell_col = e.range.columnStart;
  var data_range = ss.getRangeByName("webSchedule");

  // Check if the edits are with the web data/schedule values
  if ((data_range.getRow() <= change_cell_row == change_cell_row <= data_range.getLastRow()) &&
      (data_range.getColumn() <= change_cell_col == change_cell_col <= data_range.getLastColumn())) {
    // Run the schedule update
    scheduleService().update();
  }
}

/**
 * ---
 * Creates the sheet(s) needed to run a new season. Can be run from the
 * Add-on Menu.
 *
 */
function onNewSeason () {
  var ss = Config.spreadsheet();
  // Check that the script is up-to-date
  utils(ss).script.update();

  var sheet_name = ui().prompt.sheetname;
  var ss_triggers = ScriptApp.getUserTriggers(ss);
  var active_sheet;
   // Install add-on triggers for this spreadsheet
   if (ss_triggers.length == 0) { onUpdateTriggers() }
   // Create a template sheet if one does not exist
   if (template(ss).getTemplate() == null) { sheetService().create.template() }

  // Create a season sheet
  sheetService().create.season(sheet_name);
  // Give it time to do what it needs to do
  Utilities.sleep(250);

  // Set the new sheet as the ActiveSheet
  active_sheet = ss.getSheets()[0];
  active_sheet.showSheet();
  ss.setActiveSheet(active_sheet);

  SpreadsheetApp.flush();
  Utilities.sleep(250);

  // Copy the returning squad mates to the new season sheet
  teamService().squad.setReturning();

  Utilities.sleep(250);

  // Set the new web/online schedule link & team name
  onWeblinkUpdate();

  // Create a team form if one is not attached
  if (ss.getFormUrl() == null) { onCreateForm() }

  // Set the sheet edit protection & data validation
  sheetService().update.protection();
  sheetService().update.validation();

  // Let the user know the set-up is finished
  ui().alert.done();
}

/*******************************************************************************
*                       Menu Options - Advanced Features                       *
*******************************************************************************/

/**
 * --- Menu Option: "Extra Options Help" */
function onOptionsHelp () { ui().modal.optionsHelp() }

/**
 * --- Menu Option: "Re-install Triggers" */
function onUpdateTriggers () { utils().script.install.triggers() }

/**
 * --- Menu Option: "Remove Triggers" */
function onRemoveTriggers () { utils().script.clean.triggers() }

/**
 * --- Menu Option: "Remove Conditional Formatting" */
function onRemoveCondFormatting () { sheetService().clear.conditionalFormatting() }

/**
 * --- Menu Option: "Remove Dependencies [soft un-install]" */
function onRemoveDependencies () {
  utils().script.clean.triggers();
  settings().clearAll();
  formService().removeForm();

  SpreadsheetApp.flush();
  Utilities.sleep(500);

  sheetService().remove.sheet(settings().sheet.template_name);
}

/**
 * --- Menu Option: "Restore Data Validation" */
function onUpdateDataValidation () { sheetService().update.validation() }

/**
 * --- Menu Option: "Restore NamedRanges" */
function onUpdateNamedRange () { sheetService().update.namedRanges() }

/**
 * --- Menu Option: "Restore Conditional Formatting" */
function onUpdateCondFormatting () { sheetService().update.conditionalFormatting() }

/**
 * ---
 * Add-on Menu option to manually run the schedule update.
 */
function onRunSchedule () {
  scheduleService().update();
}

/**
 * ---
 * Add-on Menu option to manually send debt emails
 */
function onRunDebtEmail () {
  var ss = Config.spreadsheet();
  var isEmpty = ss.getRangeByName("squadEmail").isBlank();

  if (!isEmpty) {
     emailService().sendMail("other", "debt");
  }
}

/**
 * ---
 * Add-on Menu option to manually send emails for the next gameday
 */
function onRunNextEmail () {
  var ss = Config.spreadsheet();
  var isEmpty = ss.getRangeByName("squadEmail").isBlank();

  if (!isEmpty && ui().confirm.sendemail() == true) {
     emailService().sendMail("next");
  }
}

/**
 * ---
 * Add-on Menu option to manually send `returning next season` emails
 */
function onRunReturningEmail () {
  var ss = Config.spreadsheet();
  var isEmpty = ss.getRangeByName("squadEmail").isBlank();

  if (!isEmpty) {
     emailService().sendMail("other", "returning");
  }
}

/**
 * ---
 * Prompts the user to enter the link for the online schedule,
 * sets it in the sheet, and sets the team name in the spreadsheet and
 * within the sheet's attached Form from the resulting schedule.
 *
 */
function onWeblinkUpdate () {
  var name_prompt = ui().confirm.teamname;
  var isConfirmed = ui().confirm.clearall;

  // Prompt to clear schedule range if it is not blank
  if (!scheduleService().isScheduleBlank()) {
    if (!isConfirmed()) { return }

    sheetService().clear.scheduleRanges();
    // Reset data validation and protections
    sheetService().update.protection();
    sheetService().update.validation();
  }

  // Prompt and set the web link
  scheduleService().setWebLink(ui().prompt.weblink());
  teamService().updateName(name_prompt);

  SpreadsheetApp.flush();
  Utilities.sleep(500);

  // Update the form title to match the team name
  formService().updateTitle(team().name);

  // Update displayed schedule
  onRunSchedule();
}

/**
 * ---
 * Detaches the active form from the spreadsheet before
 * creating a new form and attaching it.
 */
function onResetForm () {
  // Detach the attached form and clear the response sheet
  formService().removeForm();

  SpreadsheetApp.flush();
  Utilities.sleep(500);
  // Create Team Form
  onCreateForm();
}

/**
 * ---
 * Creating a new form and attaches it to the spreadsheet
 */
 function onCreateForm () {
  // Create Team Form
  formService().createForm();

  SpreadsheetApp.flush();
  Utilities.sleep(500);
  // Update the form confirmation message
  formService().updateConfirmation();
}<|endoftext|>


<commit_before>/// <reference path="../ext/ext-core-debug.js"/>
/// <reference path="../Simplate.js"/>
/// <reference path="../sdata/SDataResourceCollectionRequest.js"/>
/// <reference path="../sdata/SDataService.js"/>
/// <reference path="View.js"/>

Ext.namespace('Sage.Platform.Mobile');
Ext.namespace('Sage.Platform.Mobile.Controls');

// todo: move to separate files
Sage.Platform.Mobile.Controls.Field = function(o) {
    Ext.apply(this, o, {
    });
};

Sage.Platform.Mobile.Controls.Field.prototype = {
    apply: function(external) {
        return this.template.apply(this);
    },
    bind: function(container) {
        this.el = container.child(String.format('input[name="{0}"]', this.name));
    },
    isDirty: function() {
        return true;
    },
    validate: function(value) {
        if (typeof this.validator === 'undefined')
            return false;

        if (this.validator instanceof RegExp)
            var definition = {
                test: this.validator
            };
        else if (typeof this.validator === 'function')
            var definition = {
                fn: this.validator
            };
        else
            var definition = this.validator;

        var value = typeof value === 'undefined'
            ? this.getValue()
            : value;

        if (typeof definition.fn === 'function')
        {
            return definition.fn.call(definition.scope || this, value, this.editor);
        }
        else if (definition.test instanceof RegExp)
        {
            if (!definition.test.test(value))
            {
                var message = typeof definition.message === 'function'
                    ? definition.message.call(definition.scope || this, value, field)
                    : String.format(definition.message, value, field);

                return message || true;
            }
        }

        return false;
    }
};

Sage.Platform.Mobile.Controls.TextField = Ext.extend(Sage.Platform.Mobile.Controls.Field, {
    template: new Simplate([
        '<input type="text" name="{%= name %}">',
    ]),
    bind: function(container) {
        Sage.Platform.Mobile.Controls.TextField.superclass.bind.apply(this, arguments);

        if (this.validInputOnly)
        {
            this.el.on('keypress', this.onKeyPress, this);
        }
        else
        {
            switch (this.validationTrigger)
            {
                case 'keyup':
                    this.el.on('keyup', this.onValidationTrigger, this);
                    break;
                case 'blur':
                    this.el.on('blur', this.onValidationTrigger, this);
                    break;
            }
        }
    },
    onKeyPress: function(evt, el, o) {
        if (this.validInputOnly)
        {
            var v = this.getValue() + String.fromCharCode(evt.getCharCode());
            if (this.validate(v))
            {
                evt.stopEvent();
                return;
            }
        }
    },
    onValidationTrigger: function(evt, el, o) {
        if (this.validate())
            this.el.addClass('field-error');
        else
            this.el.removeClass('field-error');
    },
    getValue: function() {
        return this.el.getValue();
    },
    setValue: function(val) {
        this.value = val;

        this.el.dom.value = val;
    },
    isDirty: function() {
        return (this.value != this.getValue());
    }
});

Sage.Platform.Mobile.Controls.registered = {
    'text': Sage.Platform.Mobile.Controls.TextField
};

Sage.Platform.Mobile.Edit = Ext.extend(Sage.Platform.Mobile.View, {
    viewTemplate: new Simplate([
        '
',
        '<fieldset class="loading">',
        '

{%= loadingText %}

',
        '</fieldset>',
        '
',
        '
',
        '
'
    ]),
    sectionBeginTemplate: new Simplate([
        '
{%= $["title"] %}
',
        '{% if ($["list"]) { %}
{% } else { %}<fieldset>{% } %}'
    ]),
    sectionEndTemplate: new Simplate([
        '{% if ($["list"]) { %}
{% } else { %}</fieldset>{% } %}'
    ]),
    propertyTemplate: new Simplate([
        '
',
        '{%= label %}',
        '{%! field %}', /* apply sub-template */
        '
'
    ]),
    textFieldTemplate: new Simplate([
        '<input type="text" name="{%= name %}">'
    ]),
    saveText: 'Save',
    titleText: 'Edit',
    detailsText: 'Details',
    loadingText: 'loading...',
    constructor: function(o) {
        Sage.Platform.Mobile.Edit.superclass.constructor.call(this);

        Ext.apply(this, o, {
            id: 'generic_edit',
            title: this.titleText,
            expose: false,
            tools: {
                tbar: [{
                    name: 'edit',
                    title: this.saveText,
                    cls: 'button blueButton',
                    fn: this.save,
                    scope: this
                }]
            },
            fields: {}
        });
    },
    render: function() {
        Sage.Platform.Mobile.Edit.superclass.render.call(this);

        this.bodyEl = this.el.child('.body').setVisibilityMode(Ext.Element.DISPLAY);
        this.loadEl = this.el.child('.loading').setVisibilityMode(Ext.Element.DISPLAY);
    },
    init: function() {
        Sage.Platform.Mobile.Edit.superclass.init.call(this);

        this.processLayout(this.layout, {title: this.detailsText});

        for (var name in this.fields) this.fields[name].bind(this.el);

        this.loadEl.hide();
        this.bodyEl.show();

        // todo: find a better way to handle these notifications
        if (this.canSave) App.on('save', this.onSave, this);
    },
    onSave: function() {
        if (this.isActive())
            this.save();
    },
    createRequest: function() {

    },
    createTemplateRequest: function() {

    },
    processLayout: function(layout, options)
    {
        var sections = [];
        var content = [];

        content.push(this.sectionBeginTemplate.apply(options));

        for (var i = 0; i < layout.length; i++)
        {
            var current = layout[i];

            if (current['as'])
            {
                sections.push(current);
                continue;
            }
            else
            {
                var ctor = Sage.Platform.Mobile.Controls.registered[current['type']];
                var field = this.fields[current['name']] = new ctor(Ext.apply({
                    editor: this
                }, current));

                content.push(this.propertyTemplate.apply({
                    label: current['label'],
                    field: field
                }));
            }
        }

        content.push(this.sectionEndTemplate.apply(options));

        Ext.DomHelper.append(this.el, content.join(''));

        for (var i = 0; i < sections.length; i++)
        {
            var current = sections[i];

            this.processLayout(current['as'], current['options']);
        }
    },
    requestFailure: function(response, o) {

    },
    requestData: function() {
        var request = this.createRequest();
        if (request)
            request.read({
                success: function(entry) {
                },
                failure: function(response, o) {
                    this.requestFailure(response, o);
                },
                scope: this
            });
    },
    show: function(o) {
        if (typeof o !== 'undefined')
        {
            this.entry = o;
            this.newContext = true;
        }
        else
        {
            this.newContext = false;
        }

        Sage.Platform.Mobile.Edit.superclass.show.call(this);
    },
    isNewContext: function() {
        return this.newContext;
    },
    beforeTransitionTo: function() {
        Sage.Platform.Mobile.Edit.superclass.beforeTransitionTo.call(this);
    },
    setValues: function(o) {
        for (var name in this.fields)
        {
            var value = Sage.Platform.Mobile.Utility.getValue(o, name);

            this.fields[name].setValue(value);
        }
    },
    getValues: function() {
        var o = {};
        var empty = true;

        for (var name in this.fields)
        {
            if (this.fields[name].isDirty())
            {
                var value = this.fields[name].getValue();

                Sage.Platform.Mobile.Utility.setValue(o, name, value);

                empty = false;
            }
        }
        return empty ? false : o;
    },
    validate: function() {
        this.errors = [];

        for (var name in this.fields)
        {
            var result;
            if (false !== (result = this.fields[name].validate()))
            {
                this.fields[name].el.addClass('field-error');

                this.errors.push({
                    name: name,
                    message: result
                });
            }
            else
            {
                this.fields[name].el.removeClass('field-error');
            }
        }

        return this.errors.length > 0
            ? this.errors
            : false;
    },
    createEntryForUpdate: function(values) {
        return Ext.apply(values, {
            '$key': this.entry['$key'],
            '$etag': this.entry['$etag'],
            '$name': this.entry['$name']
        });
    },
    save: function() {
        if (this.busy)
            return;

        if (this.validate() !== false)
        {
            this.el.addClass('form-error');
            return;
        }
        else
        {
            this.el.removeClass('form-error');
        }

        var values = this.getValues();
        if (values)
        {
            this.busy = true;
            this.el.addClass('view-busy');
            if (App.tbar)
                App.tbar.el.addClass('toolbar-busy');

            var entry = this.createEntryForUpdate(values);

            var request = this.createRequest();
            if (request)
                request.update(entry, {
                    success: function(modified) {
                        this.busy = false;
                        this.el.removeClass('view-busy');
                        if (App.tbar)
                            App.tbar.el.removeClass('toolbar-busy');

                        App.fireEvent('refresh', {
                            resourceKind: this.resourceKind,
                            key: modified['$key'],
                            data: {
                                '$descriptor': modified['$descriptor']
                            }
                        });

                        ReUI.back();
                    },
                    failure: function(response, o) {
                        this.busy = false;
                        this.el.removeClass('view-busy');
                        if (App.tbar)
                            App.tbar.el.removeClass('toolbar-busy');
                    },
                    scope: this
                });
        }
        else
        {
            ReUI.back();
        }
    },
    transitionTo: function() {
        Sage.Platform.Mobile.Edit.superclass.transitionTo.call(this);

        if (this.isNewContext())
        {
            this.setValues(this.entry);
        }

        // todo: check to see if we are creating instead of editing and, if so, request the 'template'
        //       from SData.
    },
    clear: function() {
        // todo: add back if we are creating instead of editing.
        // this.el.update(this.contentTemplate.apply(this));
    }
});<commit_msg>[#4095227] Added support for Phone Field.<commit_after>/// <reference path="../ext/ext-core-debug.js"/>
/// <reference path="../Simplate.js"/>
/// <reference path="../sdata/SDataResourceCollectionRequest.js"/>
/// <reference path="../sdata/SDataService.js"/>
/// <reference path="View.js"/>

Ext.namespace('Sage.Platform.Mobile');
Ext.namespace('Sage.Platform.Mobile.Controls');

// todo: move to separate files
Sage.Platform.Mobile.Controls.Field = function(o) {
    Ext.apply(this, o, {
    });
};

Sage.Platform.Mobile.Controls.Field.prototype = {
    apply: function(external) {
        return this.template.apply(this);
    },
    bind: function(container) {
        this.el = container.child(String.format('input[name="{0}"]', this.name));
    },
    isDirty: function() {
        return true;
    },
    validate: function(value) {
        if (typeof this.validator === 'undefined')
            return false;

        if (this.validator instanceof RegExp)
            var definition = {
                test: this.validator
            };
        else if (typeof this.validator === 'function')
            var definition = {
                fn: this.validator
            };
        else
            var definition = this.validator;

        var value = typeof value === 'undefined'
            ? this.getValue()
            : value;

        if (typeof definition.fn === 'function')
        {
            return definition.fn.call(definition.scope || this, value, this.editor);
        }
        else if (definition.test instanceof RegExp)
        {
            if (!definition.test.test(value))
            {
                var message = typeof definition.message === 'function'
                    ? definition.message.call(definition.scope || this, value, field)
                    : String.format(definition.message, value, field);

                return message || true;
            }
        }

        return false;
    }
};

Sage.Platform.Mobile.Controls.TextField = Ext.extend(Sage.Platform.Mobile.Controls.Field, {
    template: new Simplate([
        '<input type="text" name="{%= name %}">',
    ]),
    bind: function(container) {
        Sage.Platform.Mobile.Controls.TextField.superclass.bind.apply(this, arguments);

        if (this.validInputOnly)
        {
            this.el.on('keypress', this.onKeyPress, this);
        }
        else
        {
            switch (this.validationTrigger)
            {
                case 'keyup':
                    this.el.on('keyup', this.onValidationTrigger, this);
                    break;
                case 'blur':
                    this.el.on('blur', this.onValidationTrigger, this);
                    break;
            }
        }
    },
    onKeyPress: function(evt, el, o) {
        if (this.validInputOnly)
        {
            var v = this.getValue() + String.fromCharCode(evt.getCharCode());
            if (this.validate(v))
            {
                evt.stopEvent();
                return;
            }
        }
    },
    onValidationTrigger: function(evt, el, o) {
        if (this.validate())
            this.el.addClass('field-error');
        else
            this.el.removeClass('field-error');
    },
    getValue: function() {
        return this.el.getValue();
    },
    setValue: function(val) {
        this.value = val;

        this.el.dom.value = val;
    },
    isDirty: function() {
        return (this.value != this.getValue());
    }
});

Sage.Platform.Mobile.Controls.PhoneField = Ext.extend(Sage.Platform.Mobile.Controls.TextField, {
    getValue: function() {
        return this.el.getValue().replace(/[^0-9x]/i,"");
    },
    bind: function(container) {
        Sage.Platform.Mobile.Controls.PhoneField.superclass.bind.apply(this, arguments);
        this.el.on('keyup', this.swapLettersWithKeypadNumbers, this);
        this.el.on('keypress', this.limitPhoneFieldLength, this);
    },
    limitPhoneFieldLength: function(evt, el, o) {
        //Allow the user to still delete and navigate, if the numbers are over 13 chars.
        var ALLOWED_KEYS = {
            "8"  : "BACKSPACE",
            "46" : "KEY_DELETE",
            "45" : "KEY_INSERT",
            "37" : "KEY_LEFT",
            "39" : "KEY_RIGHT"
        };
        if (typeof ALLOWED_KEYS[evt.getCharCode()] != "string" && this.el.dom.value.length > 13) {
            evt.stopEvent();
            return;
        }
    },
    swapLettersWithKeypadNumbers: function() {
        var phoneNumber = this.el.dom.value;
        // All keys are mapped to numbers except "X", which is for denoting Extn
        var keypadLetterToNumberMap = {
            "A": 2, "B": 2, "C": 2,
            "D": 3, "E": 3, "F": 3,
            "G": 4, "H": 4, "I": 4,
            "J": 5, "K": 5, "L": 5,
            "M": 6, "N": 6, "O": 6,
            "P": 7, "Q": 7, "R": 7, "S": 7,
            "T": 8, "U": 8, "V": 8,
            "W": 9, "Y": 9, "Z": 9
        };
        //Ignore only 'x', replace other chars with numbers
        phoneNumber = phoneNumber.replace(/[a-wyz]/i, function(letter) {
            return keypadLetterToNumberMap[letter.toUpperCase()];
        });
        this.setValue(phoneNumber);
    }
});

Sage.Platform.Mobile.Controls.registered = {
    'text': Sage.Platform.Mobile.Controls.TextField,
    'phone': Sage.Platform.Mobile.Controls.PhoneField
};

Sage.Platform.Mobile.Edit = Ext.extend(Sage.Platform.Mobile.View, {
    viewTemplate: new Simplate([
        '
',
        '<fieldset class="loading">',
        '

{%= loadingText %}

',
        '</fieldset>',
        '
',
        '
',
        '
'
    ]),
    sectionBeginTemplate: new Simplate([
        '
{%= $["title"] %}
',
        '{% if ($["list"]) { %}
{% } else { %}<fieldset>{% } %}'
    ]),
    sectionEndTemplate: new Simplate([
        '{% if ($["list"]) { %}
{% } else { %}</fieldset>{% } %}'
    ]),
    propertyTemplate: new Simplate([
        '
',
        '{%= label %}',
        '{%! field %}', /* apply sub-template */
        '
'
    ]),
    textFieldTemplate: new Simplate([
        '<input type="text" name="{%= name %}">'
    ]),
    saveText: 'Save',
    titleText: 'Edit',
    detailsText: 'Details',
    loadingText: 'loading...',
    constructor: function(o) {
        Sage.Platform.Mobile.Edit.superclass.constructor.call(this);

        Ext.apply(this, o, {
            id: 'generic_edit',
            title: this.titleText,
            expose: false,
            tools: {
                tbar: [{
                    name: 'edit',
                    title: this.saveText,
                    cls: 'button blueButton',
                    fn: this.save,
                    scope: this
                }]
            },
            fields: {}
        });
    },
    render: function() {
        Sage.Platform.Mobile.Edit.superclass.render.call(this);

        this.bodyEl = this.el.child('.body').setVisibilityMode(Ext.Element.DISPLAY);
        this.loadEl = this.el.child('.loading').setVisibilityMode(Ext.Element.DISPLAY);
    },
    init: function() {
        Sage.Platform.Mobile.Edit.superclass.init.call(this);

        this.processLayout(this.layout, {title: this.detailsText});

        for (var name in this.fields) this.fields[name].bind(this.el);

        this.loadEl.hide();
        this.bodyEl.show();

        // todo: find a better way to handle these notifications
        if (this.canSave) App.on('save', this.onSave, this);
    },
    onSave: function() {
        if (this.isActive())
            this.save();
    },
    createRequest: function() {

    },
    createTemplateRequest: function() {

    },
    processLayout: function(layout, options)
    {
        var sections = [];
        var content = [];

        content.push(this.sectionBeginTemplate.apply(options));

        for (var i = 0; i < layout.length; i++)
        {
            var current = layout[i];

            if (current['as'])
            {
                sections.push(current);
                continue;
            }
            else
            {
                var ctor = Sage.Platform.Mobile.Controls.registered[current['type']];
                var field = this.fields[current['name']] = new ctor(Ext.apply({
                    editor: this
                }, current));

                content.push(this.propertyTemplate.apply({
                    label: current['label'],
                    field: field
                }));
            }
        }

        content.push(this.sectionEndTemplate.apply(options));

        Ext.DomHelper.append(this.el, content.join(''));

        for (var i = 0; i < sections.length; i++)
        {
            var current = sections[i];

            this.processLayout(current['as'], current['options']);
        }
    },
    requestFailure: function(response, o) {

    },
    requestData: function() {
        var request = this.createRequest();
        if (request)
            request.read({
                success: function(entry) {
                },
                failure: function(response, o) {
                    this.requestFailure(response, o);
                },
                scope: this
            });
    },
    show: function(o) {
        if (typeof o !== 'undefined')
        {
            this.entry = o;
            this.newContext = true;
        }
        else
        {
            this.newContext = false;
        }

        Sage.Platform.Mobile.Edit.superclass.show.call(this);
    },
    isNewContext: function() {
        return this.newContext;
    },
    beforeTransitionTo: function() {
        Sage.Platform.Mobile.Edit.superclass.beforeTransitionTo.call(this);
    },
    setValues: function(o) {
        for (var name in this.fields)
        {
            var value = Sage.Platform.Mobile.Utility.getValue(o, name);

            this.fields[name].setValue(value);
        }
    },
    getValues: function() {
        var o = {};
        var empty = true;

        for (var name in this.fields)
        {
            if (this.fields[name].isDirty())
            {
                var value = this.fields[name].getValue();

                Sage.Platform.Mobile.Utility.setValue(o, name, value);

                empty = false;
            }
        }
        return empty ? false : o;
    },
    validate: function() {
        this.errors = [];

        for (var name in this.fields)
        {
            var result;
            if (false !== (result = this.fields[name].validate()))
            {
                this.fields[name].el.addClass('field-error');

                this.errors.push({
                    name: name,
                    message: result
                });
            }
            else
            {
                this.fields[name].el.removeClass('field-error');
            }
        }

        return this.errors.length > 0
            ? this.errors
            : false;
    },
    createEntryForUpdate: function(values) {
        return Ext.apply(values, {
            '$key': this.entry['$key'],
            '$etag': this.entry['$etag'],
            '$name': this.entry['$name']
        });
    },
    save: function() {
        if (this.busy)
            return;

        if (this.validate() !== false)
        {
            this.el.addClass('form-error');
            return;
        }
        else
        {
            this.el.removeClass('form-error');
        }

        var values = this.getValues();
        if (values)
        {
            this.busy = true;
            this.el.addClass('view-busy');
            if (App.tbar)
                App.tbar.el.addClass('toolbar-busy');

            var entry = this.createEntryForUpdate(values);

            var request = this.createRequest();
            if (request)
                request.update(entry, {
                    success: function(modified) {
                        this.busy = false;
                        this.el.removeClass('view-busy');
                        if (App.tbar)
                            App.tbar.el.removeClass('toolbar-busy');

                        App.fireEvent('refresh', {
                            resourceKind: this.resourceKind,
                            key: modified['$key'],
                            data: {
                                '$descriptor': modified['$descriptor']
                            }
                        });

                        ReUI.back();
                    },
                    failure: function(response, o) {
                        this.busy = false;
                        this.el.removeClass('view-busy');
                        if (App.tbar)
                            App.tbar.el.removeClass('toolbar-busy');
                    },
                    scope: this
                });
        }
        else
        {
            ReUI.back();
        }
    },
    transitionTo: function() {
        Sage.Platform.Mobile.Edit.superclass.transitionTo.call(this);

        if (this.isNewContext())
        {
            this.setValues(this.entry);
        }

        // todo: check to see if we are creating instead of editing and, if so, request the 'template'
        //       from SData.
    },
    clear: function() {
        // todo: add back if we are creating instead of editing.
        // this.el.update(this.contentTemplate.apply(this));
    }
});<|endoftext|>