<commit_before>/* @flow */

import clone from 'lodash.clone'
import isEqual from 'lodash.isequal'
import pick from 'lodash.pick'
import path from 'path'

import logger from './logger'
import { TRASH_DIR_NAME } from './remote/constants'
import { sameDate } from './timestamp'

const log = logger({
  component: 'Metadata'
})

export type MetadataRemoteInfo = {
  _id: string,
  _rev: string
}

export type MetadataSidesInfo = {
  remote?: number,
  local?: number
}

// The files/dirs metadata, as stored in PouchDB
export type Metadata = {
  _deleted?: true,
  _id: string,
  _rev: string,
  md5sum?: string,
  class?: string,
  creationDate: string|Date,
  // TODO: v3: Use the same local *type fields as the remote ones
  docType: string,
  errors: number,
  executable?: boolean,
  lastModification: string|Date,
  mime?: string,
  moveTo?: string, // Destination id
  path: string,
  remote: MetadataRemoteInfo,
  size?: string,
  tags: string[],
  sides: MetadataSidesInfo,
  toBeTrashed?: true
}

export let buildId: (doc: Metadata) => void = (_) => {}

switch (process.platform) {
  case 'linux': case 'freebsd': case 'sunos':
    buildId = buildIdUnix
    break
  case 'darwin':
    buildId = buildIdHFS
    break
  case 'win32':
    buildId = buildIdNTFS
    break
  default:
    log.error(`Sorry, ${process.platform} is not supported!`)
    process.exit(1)
}

// Build an _id from the path for a case sensitive file system (Linux, BSD)
function buildIdUnix (doc: Metadata) {
  doc._id = doc.path
}

// Build an _id from the path for OSX (HFS+ file system):
// - case preservative, but not case sensitive
// - unicode NFD normalization (sort of)
//
// See https://nodejs.org/en/docs/guides/working-with-different-filesystems/
// for why toUpperCase is better than toLowerCase
//
// Note: String.prototype.normalize is not available on node 0.10 and does
// nothing when node is compiled without intl option.
function buildIdHFS (doc: Metadata) {
  let id = doc.path
  if (id.normalize) { id = id.normalize('NFD') }
  doc._id = id.toUpperCase()
}

// Build an _id from the path for Windows (NTFS file system)
function buildIdNTFS (doc: Metadata) {
  doc._id = doc.path.toUpperCase()
}

// Return true if the document has not a valid path
// (ie a path inside the mount point)
export function invalidPath (doc: Metadata) {
  if (!doc.path) { return true }
  doc.path = path.normalize(doc.path)
  if (doc.path.startsWith(path.sep)) {
    doc.path = doc.path.slice(1)
  }
  let parts = doc.path.split(path.sep)
  return (doc.path === '.') ||
          (doc.path === '') ||
          (parts.indexOf('..') >= 0)
}

export function ensureValidPath (doc: Metadata) {
  if (invalidPath(doc)) {
    log.warn(`Invalid path: ${JSON.stringify(doc, null, 2)}`)
    throw new Error('Invalid path')
  }
}

// Return true if the checksum is invalid
// If the checksum is missing, it is not invalid, just missing,
// so it returns false.
// MD5 has 16 bytes.
// Base64 encoding must include padding.
export function invalidChecksum (doc: Metadata) {
  if (doc.md5sum == null) return false

  const buffer = Buffer.from(doc.md5sum, 'base64')

  return buffer.byteLength !== 16 ||
    buffer.toString('base64') !== doc.md5sum
}

export function ensureValidChecksum (doc: Metadata) {
  if (invalidChecksum(doc)) {
    log.warn(`Invalid checksum: ${JSON.stringify(doc, null, 2)}`)
    throw new Error('Invalid checksum')
  }
}

// Extract the revision number, or 0 it not found
export function extractRevNumber (infos: Metadata) {
  try {
    let rev = infos._rev.split('-')[0]
    return Number(rev)
  } catch (error) {
    return 0
  }
}

// Return true if the remote file is up-to-date for this document
export function isUpToDate (doc: Metadata) {
  let currentRev = doc.sides.remote || 0
  let lastRev = extractRevNumber(doc)
  return currentRev === lastRev
}

// Return true if the metadata of the two folders are the same
// The creationDate of the two folders are not compared, because the local
// filesystem can't give us a relevant information for that.
// For lastModification, we accept up to 3s of differences because we can't
// rely on file systems to be precise to the millisecond.
export function sameFolder (one: Metadata, two: Metadata) {
  if (!sameDate(one.lastModification, two.lastModification)) { return false }
  let fields = ['_id', 'docType', 'remote', 'tags']
  one = pick(one, fields)
  two = pick(two, fields)
  const same = isEqual(one, two)
  if (!same) log.debug({diff: {one, two}})
  return same
}

// Return true if the metadata of the two files are the same
// The creationDate of the two files are not compared, because the local
// filesystem can't give us a relevant information for that.
// For lastModification, we accept up to 3s of differences because we can't
// rely on file systems to be precise to the millisecond.
export function sameFile (one: Metadata, two: Metadata) {
  if (!sameDate(one.lastModification, two.lastModification)) { return false }
  let fields = ['_id', 'docType', 'md5sum', 'remote', 'tags', 'size']
  one = {...pick(one, fields), executable: !!one.executable}
  two = {...pick(two, fields), executable: !!two.executable}
  const same = isEqual(one, two)
  if (!same) log.debug({diff: {one, two}})
  return same
}

// Return true if the two files have the same binary content
export function sameBinary (one: Metadata, two: Metadata) {
  if ((one.docType !== 'file') || (two.docType !== 'file')) {
    return false
  } else if ((one.md5sum != null) && (one.md5sum === two.md5sum)) {
    return true
  } else if ((one.remote != null) && (two.remote != null)) {
    let oneId = one.remote._id
    let twoId = two.remote._id
    return (oneId != null) && (oneId === twoId)
  } else {
    return false
  }
}

// Mark the next rev for this side
//
// To track which side has made which modification, a revision number is
// associated to each side. When a side make a modification, we extract the
// revision from the previous state, increment it by one to have the next
// revision and associate this number to the side that makes the
// modification.
export function markSide (side: string, doc: Metadata, prev: ?Metadata): Metadata {
  let rev = 0
  if (prev) { rev = extractRevNumber(prev) }
  if (doc.sides == null) {
    const was = prev && prev.sides
    doc.sides = clone(was || {})
  }
  doc.sides[side] = ++rev
  return doc
}

export function inRemoteTrash (doc: Metadata): boolean {
  return doc.path.startsWith(TRASH_DIR_NAME)
}
<commit_msg>metadata: legacy size field is a number, not a string<commit_after>/* @flow */

import clone from 'lodash.clone'
import isEqual from 'lodash.isequal'
import pick from 'lodash.pick'
import path from 'path'

import logger from './logger'
import { TRASH_DIR_NAME } from './remote/constants'
import { sameDate } from './timestamp'

const log = logger({
  component: 'Metadata'
})

export type MetadataRemoteInfo = {
  _id: string,
  _rev: string
}

export type MetadataSidesInfo = {
  remote?: number,
  local?: number
}

// The files/dirs metadata, as stored in PouchDB
export type Metadata = {
  _deleted?: true,
  _id: string,
  _rev: string,
  md5sum?: string,
  class?: string,
  creationDate: string|Date,
  // TODO: v3: Use the same local *type fields as the remote ones
  docType: string,
  errors: number,
  executable?: boolean,
  lastModification: string|Date,
  mime?: string,
  moveTo?: string, // Destination id
  path: string,
  remote: MetadataRemoteInfo,
  size?: number,
  tags: string[],
  sides: MetadataSidesInfo,
  toBeTrashed?: true
}

export let buildId: (doc: Metadata) => void = (_) => {}

switch (process.platform) {
  case 'linux': case 'freebsd': case 'sunos':
    buildId = buildIdUnix
    break
  case 'darwin':
    buildId = buildIdHFS
    break
  case 'win32':
    buildId = buildIdNTFS
    break
  default:
    log.error(`Sorry, ${process.platform} is not supported!`)
    process.exit(1)
}

// Build an _id from the path for a case sensitive file system (Linux, BSD)
function buildIdUnix (doc: Metadata) {
  doc._id = doc.path
}

// Build an _id from the path for OSX (HFS+ file system):
// - case preservative, but not case sensitive
// - unicode NFD normalization (sort of)
//
// See https://nodejs.org/en/docs/guides/working-with-different-filesystems/
// for why toUpperCase is better than toLowerCase
//
// Note: String.prototype.normalize is not available on node 0.10 and does
// nothing when node is compiled without intl option.
function buildIdHFS (doc: Metadata) {
  let id = doc.path
  if (id.normalize) { id = id.normalize('NFD') }
  doc._id = id.toUpperCase()
}

// Build an _id from the path for Windows (NTFS file system)
function buildIdNTFS (doc: Metadata) {
  doc._id = doc.path.toUpperCase()
}

// Return true if the document has not a valid path
// (ie a path inside the mount point)
export function invalidPath (doc: Metadata) {
  if (!doc.path) { return true }
  doc.path = path.normalize(doc.path)
  if (doc.path.startsWith(path.sep)) {
    doc.path = doc.path.slice(1)
  }
  let parts = doc.path.split(path.sep)
  return (doc.path === '.') ||
          (doc.path === '') ||
          (parts.indexOf('..') >= 0)
}

export function ensureValidPath (doc: Metadata) {
  if (invalidPath(doc)) {
    log.warn(`Invalid path: ${JSON.stringify(doc, null, 2)}`)
    throw new Error('Invalid path')
  }
}

// Return true if the checksum is invalid
// If the checksum is missing, it is not invalid, just missing,
// so it returns false.
// MD5 has 16 bytes.
// Base64 encoding must include padding.
export function invalidChecksum (doc: Metadata) {
  if (doc.md5sum == null) return false

  const buffer = Buffer.from(doc.md5sum, 'base64')

  return buffer.byteLength !== 16 ||
    buffer.toString('base64') !== doc.md5sum
}

export function ensureValidChecksum (doc: Metadata) {
  if (invalidChecksum(doc)) {
    log.warn(`Invalid checksum: ${JSON.stringify(doc, null, 2)}`)
    throw new Error('Invalid checksum')
  }
}

// Extract the revision number, or 0 it not found
export function extractRevNumber (infos: Metadata) {
  try {
    let rev = infos._rev.split('-')[0]
    return Number(rev)
  } catch (error) {
    return 0
  }
}

// Return true if the remote file is up-to-date for this document
export function isUpToDate (doc: Metadata) {
  let currentRev = doc.sides.remote || 0
  let lastRev = extractRevNumber(doc)
  return currentRev === lastRev
}

// Return true if the metadata of the two folders are the same
// The creationDate of the two folders are not compared, because the local
// filesystem can't give us a relevant information for that.
// For lastModification, we accept up to 3s of differences because we can't
// rely on file systems to be precise to the millisecond.
export function sameFolder (one: Metadata, two: Metadata) {
  if (!sameDate(one.lastModification, two.lastModification)) { return false }
  let fields = ['_id', 'docType', 'remote', 'tags']
  one = pick(one, fields)
  two = pick(two, fields)
  const same = isEqual(one, two)
  if (!same) log.debug({diff: {one, two}})
  return same
}

// Return true if the metadata of the two files are the same
// The creationDate of the two files are not compared, because the local
// filesystem can't give us a relevant information for that.
// For lastModification, we accept up to 3s of differences because we can't
// rely on file systems to be precise to the millisecond.
export function sameFile (one: Metadata, two: Metadata) {
  if (!sameDate(one.lastModification, two.lastModification)) { return false }
  let fields = ['_id', 'docType', 'md5sum', 'remote', 'tags', 'size']
  one = {...pick(one, fields), executable: !!one.executable}
  two = {...pick(two, fields), executable: !!two.executable}
  const same = isEqual(one, two)
  if (!same) log.debug({diff: {one, two}})
  return same
}

// Return true if the two files have the same binary content
export function sameBinary (one: Metadata, two: Metadata) {
  if ((one.docType !== 'file') || (two.docType !== 'file')) {
    return false
  } else if ((one.md5sum != null) && (one.md5sum === two.md5sum)) {
    return true
  } else if ((one.remote != null) && (two.remote != null)) {
    let oneId = one.remote._id
    let twoId = two.remote._id
    return (oneId != null) && (oneId === twoId)
  } else {
    return false
  }
}

// Mark the next rev for this side
//
// To track which side has made which modification, a revision number is
// associated to each side. When a side make a modification, we extract the
// revision from the previous state, increment it by one to have the next
// revision and associate this number to the side that makes the
// modification.
export function markSide (side: string, doc: Metadata, prev: ?Metadata): Metadata {
  let rev = 0
  if (prev) { rev = extractRevNumber(prev) }
  if (doc.sides == null) {
    const was = prev && prev.sides
    doc.sides = clone(was || {})
  }
  doc.sides[side] = ++rev
  return doc
}

export function inRemoteTrash (doc: Metadata): boolean {
  return doc.path.startsWith(TRASH_DIR_NAME)
}
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import {AutoSizer, Column, Table, SortDirection} from 'react-virtualized';
import {apiFetchJson} from '../helpers/apiFetchJson';
import {rankedByGradeLevel} from '../helpers/SortHelpers';
import {maybeCapitalize} from '../helpers/pretty';
import {supportsHouse, supportsCounselor, supportsSpedLiaison} from '../helpers/PerDistrict';
import {prettyProgramOrPlacementText} from '../helpers/specialEducation';
import {updateGlobalStylesToTakeFullHeight} from '../helpers/globalStylingWorkarounds';
import GenericLoader from '../components/GenericLoader';
import SectionHeading from '../components/SectionHeading';
import HouseBadge from '../components/HouseBadge';
import School from '../components/School';
import StudentPhotoCropped from '../components/StudentPhotoCropped';
import FilterStudentsBar from './FilterStudentsBar';

// Shows a list of students for the educator.  Intended as a directory
// for navigation, showing everything that's in the student searchbar all at once.
// This isn't for doing analysis or looking at data.
export default class MyStudentsPage extends React.Component {
  constructor(props) {
    super(props);
    this.fetchStudents = this.fetchStudents.bind(this);
    this.renderStudents = this.renderStudents.bind(this);
  }

  componentDidMount() {
    updateGlobalStylesToTakeFullHeight();
  }

  fetchStudents() {
    const url = `/api/educators/my_students_json`;
    return apiFetchJson(url);
  }

  render() {
    return (

        <GenericLoader
          promiseFn={this.fetchStudents}
          style={styles.flexVertical}
          render={this.renderStudents} />

    );
  }

  renderStudents(json) {
    const {students} = json;
    return <MyStudentsPageView students={students} />;
  }
}

export class MyStudentsPageView extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      sortBy: 'name',
      sortDirection: SortDirection.ASC,
    };
    this.onTableSort = this.onTableSort.bind(this);
    this.renderName = this.renderName.bind(this);
    this.renderSchool = this.renderSchool.bind(this);
  }

  orderedStudents(students) {
    const {sortBy, sortDirection} = this.state;

    // map dataKey to an accessor/sort function
    const sortFns = {
      fallback(student) { return student[sortBy]; },
      grade(student) { return rankedByGradeLevel(student.grade); },
      school(student) { return student.school.name; },
      name(student) { return `${student.last_name}, ${student.first_name}`; }
    };
    const sortFn = sortFns[sortBy] || sortFns.fallback;
    const sortedRows = _.sortBy(students, sortFn);

    // respect direction
    return (sortDirection == SortDirection.DESC)
      ? sortedRows.reverse()
      : sortedRows;
  }

  onTableSort({defaultSortDirection, event, sortBy, sortDirection}) {
    if (sortBy === this.state.sortBy) {
      const oppositeSortDirection = (this.state.sortDirection == SortDirection.DESC)
        ? SortDirection.ASC
        : SortDirection.DESC;
      this.setState({ sortDirection: oppositeSortDirection });
    } else {
      this.setState({sortBy});
    }
  }

  render() {
    const {districtKey} = this.context;
    const {students} = this.props;

    return (

        <SectionHeading>My students</SectionHeading>
        <FilterStudentsBar
          students={students}
          style={{...styles.flexVertical, marginLeft: 10, marginTop: 20}}
          includeHouse={supportsHouse(districtKey)}
          includeCounselor={supportsCounselor(districtKey)}>
          {filteredStudents => this.renderTable(filteredStudents)}
        </FilterStudentsBar>

    );
  }

  renderTable(filteredStudents) {
    const {districtKey} = this.context;
    const {sortDirection, sortBy} = this.state;
    const sortedStudents = this.orderedStudents(filteredStudents);
    const rowHeight = 40; // for two lines of student names

    // In conjuction with the filtering, this can lead to a warning in development.
    // See https://github.com/bvaughn/react-virtualized/issues/1119 for more.
    return (
      <AutoSizer style={{marginTop: 20}}>
        {({width, height}) => (

 sortedStudents[index]}
            sort={this.onTableSort}
            sortBy={sortBy}
            sortDirection={sortDirection}
            >
            <Column
              label=''
              dataKey='photo'
              cellRenderer={this.renderPhoto}
              width={60}
            />
            <Column
              label='Name'
              dataKey='name'
              cellRenderer={this.renderName}
              flexGrow={1}
              width={170}
            />
            <Column
              label='School'
              dataKey='school'
              cellRenderer={this.renderSchool}
              width={140}
            />
            <Column
              label='Grade'
              dataKey='grade'
              width={100}
            />
            <Column
             label='Program'
             dataKey='program'
             cellRenderer={this.renderProgram}
             width={150}
            />
            {supportsHouse(districtKey) &&
              <Column
                label='House'
                dataKey='house'
                cellRenderer={this.renderHouse}
                width={120} />
            }
            {supportsCounselor(districtKey) &&
              <Column
                label='Counselor'
                dataKey='counselor'
                cellDataGetter={({rowData}) => maybeCapitalize(rowData.counselor)}
                width={100}
              />
            }
            {supportsSpedLiaison(districtKey) &&
              <Column
                label='SPED Liaison'
                dataKey='sped_liaison'
                cellDataGetter={({rowData}) => maybeCapitalize(rowData.sped_liaison)}
                width={100}
              />
            }

        )}
      </AutoSizer>
    );
  }

  renderName(cellProps) {
    const student = cellProps.rowData;
    return <a style={{fontSize: 14}} href={`/students/${student.id}`} target="_blank" rel="noopener noreferrer">{student.first_name} {student.last_name};
  }

  renderPhoto(cellProps) {
    const student = cellProps.rowData;
    return <StudentPhotoCropped studentId={student.id} style={{width: 32, height: 32}} />;
  }

  renderSchool(cellProps) {
    const student = cellProps.rowData;
    return <School {...student.school} />;
  }

  renderHouse(cellProps) {
    const student = cellProps.rowData;
    return student.house && <HouseBadge house={student.house} showNameOnly={true} />;
  }

  renderProgram(cellProps) {
    const student = cellProps.rowData;
    return prettyProgramOrPlacementText(student);
  }
}
MyStudentsPageView.contextTypes = {
  districtKey: PropTypes.string.isRequired
};
MyStudentsPageView.propTypes = {
  students: PropTypes.arrayOf(PropTypes.shape({
    id: PropTypes.number.isRequired,
    first_name: PropTypes.string.isRequired,
    last_name: PropTypes.string.isRequired,
    house: PropTypes.string,
    counselor: PropTypes.string,
    grade: PropTypes.string.isRequired,
    school: PropTypes.shape({
      id: PropTypes.number.isRequired,
      name: PropTypes.string.isRequired
    }).isRequired,
  })).isRequired
};

const styles = {
  flexVertical: {
    display: 'flex',
    flex: 1,
    flexDirection: 'column'
  }
};<commit_msg>Update My students with photo<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import {AutoSizer, Column, Table, SortDirection} from 'react-virtualized';
import {apiFetchJson} from '../helpers/apiFetchJson';
import {rankedByGradeLevel} from '../helpers/SortHelpers';
import {maybeCapitalize} from '../helpers/pretty';
import {supportsHouse, supportsCounselor, supportsSpedLiaison} from '../helpers/PerDistrict';
import {prettyProgramOrPlacementText} from '../helpers/specialEducation';
import {updateGlobalStylesToTakeFullHeight} from '../helpers/globalStylingWorkarounds';
import GenericLoader from '../components/GenericLoader';
import SectionHeading from '../components/SectionHeading';
import HouseBadge from '../components/HouseBadge';
import School from '../components/School';
import StudentPhotoCropped from '../components/StudentPhotoCropped';
import FilterStudentsBar from './FilterStudentsBar';

// Shows a list of students for the educator.  Intended as a directory
// for navigation, showing everything that's in the student searchbar all at once.
// This isn't for doing analysis or looking at data.
export default class MyStudentsPage extends React.Component {
  constructor(props) {
    super(props);
    this.fetchStudents = this.fetchStudents.bind(this);
    this.renderStudents = this.renderStudents.bind(this);
  }

  componentDidMount() {
    updateGlobalStylesToTakeFullHeight();
  }

  fetchStudents() {
    const url = `/api/educators/my_students_json`;
    return apiFetchJson(url);
  }

  render() {
    return (

        <GenericLoader
          promiseFn={this.fetchStudents}
          style={styles.flexVertical}
          render={this.renderStudents} />

    );
  }

  renderStudents(json) {
    const {students} = json;
    return <MyStudentsPageView students={students} />;
  }
}

export class MyStudentsPageView extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      sortBy: 'name',
      sortDirection: SortDirection.ASC,
    };
    this.onTableSort = this.onTableSort.bind(this);
    this.renderName = this.renderName.bind(this);
    this.renderSchool = this.renderSchool.bind(this);
  }

  orderedStudents(students) {
    const {sortBy, sortDirection} = this.state;

    // map dataKey to an accessor/sort function
    const sortFns = {
      fallback(student) { return student[sortBy]; },
      grade(student) { return rankedByGradeLevel(student.grade); },
      school(student) { return student.school.name; },
      name(student) { return `${student.last_name}, ${student.first_name}`; }
    };
    const sortFn = sortFns[sortBy] || sortFns.fallback;
    const sortedRows = _.sortBy(students, sortFn);

    // respect direction
    return (sortDirection == SortDirection.DESC)
      ? sortedRows.reverse()
      : sortedRows;
  }

  onTableSort({defaultSortDirection, event, sortBy, sortDirection}) {
    if (sortBy === this.state.sortBy) {
      const oppositeSortDirection = (this.state.sortDirection == SortDirection.DESC)
        ? SortDirection.ASC
        : SortDirection.DESC;
      this.setState({ sortDirection: oppositeSortDirection });
    } else {
      this.setState({sortBy});
    }
  }

  render() {
    const {districtKey} = this.context;
    const {students} = this.props;

    return (

        <SectionHeading>My students</SectionHeading>
        <FilterStudentsBar
          students={students}
          style={{...styles.flexVertical, marginLeft: 10, marginTop: 20}}
          includeHouse={supportsHouse(districtKey)}
          includeCounselor={supportsCounselor(districtKey)}>
          {filteredStudents => this.renderTable(filteredStudents)}
        </FilterStudentsBar>

    );
  }

  renderTable(filteredStudents) {
    const {districtKey} = this.context;
    const {sortDirection, sortBy} = this.state;
    const sortedStudents = this.orderedStudents(filteredStudents);
    const rowHeight = 40; // for two lines of student names

    // In conjuction with the filtering, this can lead to a warning in development.
    // See https://github.com/bvaughn/react-virtualized/issues/1119 for more.
    return (
      <AutoSizer style={{marginTop: 20}}>
        {({width, height}) => (

 sortedStudents[index]}
            sort={this.onTableSort}
            sortBy={sortBy}
            sortDirection={sortDirection}
            >
            <Column
              label='Name'
              dataKey='name'
              cellRenderer={this.renderName}
              width={220}
            />
            <Column
              label='School'
              dataKey='school'
              cellRenderer={this.renderSchool}
              width={140}
            />
            <Column
              label='Grade'
              dataKey='grade'
              width={100}
            />
            <Column
             label='Program'
             dataKey='program'
             cellRenderer={this.renderProgram}
             width={150}
            />
            {supportsHouse(districtKey) &&
              <Column
                label='House'
                dataKey='house'
                cellRenderer={this.renderHouse}
                width={120} />
            }
            {supportsCounselor(districtKey) &&
              <Column
                label='Counselor'
                dataKey='counselor'
                cellDataGetter={({rowData}) => maybeCapitalize(rowData.counselor)}
                width={100}
              />
            }
            {supportsSpedLiaison(districtKey) &&
              <Column
                label='SPED Liaison'
                dataKey='sped_liaison'
                cellDataGetter={({rowData}) => maybeCapitalize(rowData.sped_liaison)}
                width={100}
              />
            }

        )}
      </AutoSizer>
    );
  }

  renderName(cellProps) {
    const student = cellProps.rowData;
    return (

        <a style={{fontSize: 14}} href={`/students/${student.id}`} target="_blank" rel="noopener noreferrer">{student.first_name} {student.last_name}
        <StudentPhotoCropped
          studentId={student.id}
          style={styles.photo} />

    );
  }

  renderSchool(cellProps) {
    const student = cellProps.rowData;
    return <School {...student.school} />;
  }

  renderHouse(cellProps) {
    const student = cellProps.rowData;
    return student.house && <HouseBadge house={student.house} showNameOnly={true} />;
  }

  renderProgram(cellProps) {
    const student = cellProps.rowData;
    return prettyProgramOrPlacementText(student);
  }
}
MyStudentsPageView.contextTypes = {
  districtKey: PropTypes.string.isRequired
};
MyStudentsPageView.propTypes = {
  students: PropTypes.arrayOf(PropTypes.shape({
    id: PropTypes.number.isRequired,
    first_name: PropTypes.string.isRequired,
    last_name: PropTypes.string.isRequired,
    house: PropTypes.string,
    counselor: PropTypes.string,
    grade: PropTypes.string.isRequired,
    school: PropTypes.shape({
      id: PropTypes.number.isRequired,
      name: PropTypes.string.isRequired
    }).isRequired,
  })).isRequired
};

const styles = {
  flexVertical: {
    display: 'flex',
    flex: 1,
    flexDirection: 'column'
  },
  nameBlock: {
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center'
  },
  photo: {
    width: 32,
    height: 32,
    display: 'inline-block',
    marginRight: 20
  }
};<|endoftext|>


<commit_before>/* globals util, window, document, console */
/* exported ui */

var isChrome = false;

const ui = (function () { // eslint-disable-line no-unused-vars
  let exports = {};

  // The  tag itself can have margins and offsets, which need to be used when
  // setting the position of the boxEl.
  function getBodyRect() {
    if (getBodyRect.cached) {
      return getBodyRect.cached;
    }
    let rect = document.body.getBoundingClientRect();
    let cached = {
      top: rect.top + window.scrollY,
      bottom: rect.bottom + window.scrollY,
      left: rect.left + window.scrollX,
      right: rect.right + window.scrollX
    };
    // FIXME: I can't decide when this is necessary
    // *not* necessary on http://patriciogonzalezvivo.com/2015/thebookofshaders/
    // (actually causes mis-selection there)
    // *is* necessary on http://atirip.com/2015/03/17/sorry-sad-state-of-matrix-transforms-in-browsers/
    cached = {top: 0, bottom: 0, left: 0, right: 0};
    getBodyRect.cached = cached;
    return cached;
  }

  function isHeader(el) {
    while (el) {
      if (el.className &&
          (el.className.indexOf("pageshot-saver") !== -1 ||
           el.className.indexOf("pageshot-myshots") !== -1 ||
           el.className.indexOf("pageshot-visible") !== -1 ||
           el.className.indexOf("pageshot-full-page") !== -1)) {
        return true;
      }
      el = el.parentNode;
    }
    return false;
  }
  exports.isHeader = isHeader;

  function makeEl(tagName, className) {
    let el = document.createElement(tagName);
    if (className) {
      el.className = className;
    }
    return el;
  }

  /** Represents the shadow overlay that covers the whole page */
  let WholePageOverlay = exports.WholePageOverlay = {

    display: function (callbacks, installHandlersOnDocument) {

      if (! this.overlayFrame) {
        let bodyRect = document.body.getBoundingClientRect();

        this.overlayFrame = document.createElement("iframe");
        //this.overlayFrame.style.pointerEvents = "none";
        this.overlayFrame.style.zIndex = "99999999999";
        this.overlayFrame.style.border = "none";
        this.overlayFrame.style.position = "absolute";
        this.overlayFrame.style.top = "0";
        this.overlayFrame.style.left = "0";
        this.overlayFrame.style.height = "100%";
        this.overlayFrame.style.width = "100%";

        document.body.appendChild(this.overlayFrame);

        this.overlayFrame.onload = () => {
          var parsedDom = (new DOMParser()).parseFromString(
            "<title></title>",
            "text/html"
          );
          let frameDoc = this.overlayFrame.contentDocument;

          frameDoc.replaceChild(
              frameDoc.adoptNode(parsedDom.documentElement),
              frameDoc.documentElement
          );

          let overlayEl = makeEl("div", "pageshot-preview-overlay");
          let instructions = makeEl("div", "pageshot-preview-instructions");
          instructions.textContent = "Drag or click on the page to select a region. Press ESC to cancel.";
          overlayEl.appendChild(instructions);
          let button = makeEl("div", "pageshot-myshots");
          button.addEventListener("click", callbacks.onOpenMyShots, false);
          let myShotsPre = makeEl("div", "pageshot-pre-myshots");
          button.appendChild(myShotsPre);
          let text = makeEl("div", "pageshot-myshots-text");
          text.textContent = "My Shots";
          button.appendChild(text);
          let myShotsPost = makeEl("div", "pageshot-post-myshots");
          button.appendChild(myShotsPost);
          overlayEl.appendChild(button);
          let visibleButton = makeEl("div", "pageshot-overlay-button pageshot-visible");
          visibleButton.textContent = "Save visible";
          visibleButton.addEventListener("click", callbacks.onClickVisible, false);
          overlayEl.appendChild(visibleButton);
          let fullPageButton = makeEl("div", "pageshot-overlay-button pageshot-full-page");
          fullPageButton.textContent = "Save full page";
          fullPageButton.addEventListener("click", callbacks.onClickFullPage, false);
          overlayEl.appendChild(fullPageButton);
          frameDoc.body.appendChild(overlayEl);
          let linkUrl = self.options["inline-selection.css"];
          var link = frameDoc.getElementById("pageshot-stylesheet");
          if (! link) {
            link = frameDoc.createElement("link");
            link.setAttribute("rel", "stylesheet");
            link.setAttribute("id", "pageshot-stylesheet");
            link.setAttribute("href", linkUrl);
            frameDoc.head.appendChild(link);
          }
          installHandlersOnDocument(frameDoc);
        };
      }
    },

    remove: function () {
      util.removeNode(this.overlayFrame);
      this.overlayFrame = null;
    },

    el: null
  };

  let movements = ["topLeft", "top", "topRight", "left", "right", "bottomLeft", "bottom", "bottomRight"];

  /** Creates the selection box */
  exports.Box = {

    display: function (pos, callbacks) {
      this._createEl();
      if (callbacks !== undefined && callbacks.cancel) {
        // We use onclick here because we don't want addEventListener
        // to add multiple event handlers to the same button
        this.cancel.onclick = callbacks.cancel;
        this.cancel.style.display = "";
      } else {
        this.cancel.style.display = "none";
      }
      if (callbacks !== undefined && callbacks.save) {
        // We use onclick here because we don't want addEventListener
        // to add multiple event handlers to the same button
        this.save.removeAttribute("disabled");
        this.save.onclick = (e) => {
          this.save.setAttribute("disabled", "true");
          callbacks.save(e);
        };
        this.save.style.display = "";
      } else {
        this.save.style.display = "none";
      }
      let bodyRect = getBodyRect();
      // Note, document.documentElement.scrollHeight is zero on some strange pages (such as the page created when you load an image):
      let docHeight = Math.max(document.documentElement.scrollHeight || 0, document.body.scrollHeight);
      let docWidth = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth);
      if ((pos.right - pos.left) < 78 || (pos.bottom - pos.top) < 78) {
        this.el.classList.add("pageshot-small-selection");
      } else {
        this.el.classList.remove("pageshot-small-selection");
      }
      this.el.style.top = (pos.top - bodyRect.top) + "px";
      this.el.style.left = (pos.left - bodyRect.left) + "px";
      this.el.style.height = (pos.bottom - pos.top - bodyRect.top) + "px";
      this.el.style.width = (pos.right - pos.left - bodyRect.left) + "px";
      this.bgTop.style.top = "0px";
      this.bgTop.style.height = (pos.top - bodyRect.top) + "px";
      this.bgTop.style.left = "0px";
      this.bgTop.style.width = docWidth + "px";
      this.bgBottom.style.top = (pos.bottom - bodyRect.top) + "px";
      this.bgBottom.style.height = docHeight - (pos.bottom - bodyRect.top) + "px";
      this.bgBottom.style.left = "0px";
      this.bgBottom.style.width = docWidth + "px";
      this.bgLeft.style.top = (pos.top - bodyRect.top) + "px";
      this.bgLeft.style.height = pos.bottom - pos.top  + "px";
      this.bgLeft.style.left = "0px";
      this.bgLeft.style.width = (pos.left - bodyRect.left) + "px";
      this.bgRight.style.top = (pos.top - bodyRect.top) + "px";
      this.bgRight.style.height = pos.bottom - pos.top + "px";
      this.bgRight.style.left = (pos.right - bodyRect.left) + "px";
      this.bgRight.style.width = docWidth - (pos.right - bodyRect.left) + "px";
      WholePageOverlay.remove();
    },

    remove: function () {
      for (let name of ["el", "bgTop", "bgLeft", "bgRight", "bgBottom"]) {
        util.removeNode(this[name]);
        this[name] = null;
      }
    },

    _createEl: function () {
      let boxEl = this.el;
      if (boxEl) {
        return;
      }
      boxEl = makeEl("div", "pageshot-highlight");
      let buttons = makeEl("div", "pageshot-highlight-buttons");
      let cancel = makeEl("button", "pageshot-highlight-button-cancel");
      cancel.textContent = "Cancel";
      buttons.appendChild(cancel);
      let save = makeEl("button", "pageshot-highlight-button-save");
      save.textContent = "Save";
      buttons.appendChild(save);
      this.cancel = cancel;
      this.save = save;
      boxEl.appendChild(buttons);
      for (let name of movements) {
        let elTarget = makeEl("div", "pageshot-mover-target pageshot-" + name);
        let elMover = makeEl("div", "pageshot-mover");
        elTarget.appendChild(elMover);
        boxEl.appendChild(elTarget);
      }
      this.bgTop = makeEl("div", "pageshot-bghighlight");
      document.body.appendChild(this.bgTop);
      this.bgLeft = makeEl("div", "pageshot-bghighlight");
      document.body.appendChild(this.bgLeft);
      this.bgRight = makeEl("div", "pageshot-bghighlight");
      document.body.appendChild(this.bgRight);
      this.bgBottom = makeEl("div", "pageshot-bghighlight");
      document.body.appendChild(this.bgBottom);
      document.body.appendChild(boxEl);
      this.el = boxEl;
    },

    draggerDirection: function (target) {
      while (target) {
        if (target.nodeType == document.ELEMENT_NODE) {
          if (target.classList.contains("pageshot-mover-target")) {
            for (let name of movements) {
              if (target.classList.contains("pageshot-" + name)) {
                return name;
              }
            }
            console.warn("Got pageshot-mover-target that wasn't a specific direction");
          }
        }
        target = target.parentNode;
      }
      return null;
    },

    isSelection: function (target) {
      while (target) {
        if (target.nodeType == document.ELEMENT_NODE && target.classList.contains("pageshot-highlight")) {
          return true;
        }
        target = target.parentNode;
      }
      return false;
    },

    el: null,
    boxTopEl: null,
    boxLeftEl: null,
    boxRightEl: null,
    boxBottomEl: null
  };

  exports.MyShotsReminder = {

    display: function () {
      if (this.dialogEl) {
        return;
      }
      let div = makeEl("div", "pageshot-myshots-reminder");
      if (isChrome) {
        div.className += " pageshot-myshots-reminder-chrome";
      }
      div.innerHTML = `

            Click this button to view all the shots you've taken

      `;
      document.body.appendChild(div);
      this.dialogEl = div;
    },

    remove: function () {
      util.removeNode(this.dialogEl);
      this.dialogEl = null;
    },

    dialogEl: null
  };

  exports.HoverBox = {

    el: null,

    display: function (rect) {
      if (! this.el) {
        this.el = makeEl("div", "pageshot-hover-highlight");
        document.body.appendChild(this.el);
      }
      this.el.style.display = "";
      this.el.style.top = (rect.top - 1) + "px";
      this.el.style.left = (rect.left - 1) + "px";
      this.el.style.width = (rect.right - rect.left + 2) + "px";
      this.el.style.height = (rect.bottom - rect.top + 2) + "px";
    },

    hide: function () {
      if (this.el) {
        this.el.style.display = "none";
      }
    },

    remove: function () {
      util.removeNode(this.el);
      this.el = null;
    }
  };

  /** Removes every UI this module creates */
  exports.remove = function () {
    for (let name in exports) {
      if (typeof exports[name] == "object" && exports[name].remove) {
        exports[name].remove();
      }
    }
  };

  exports.ChromeInterface = {

    onMyShots: null,
    onSave: null,
    onCancel: null,

    display: function () {
      if (! this.el) {
        this.el = makeEl("div", "pageshot-saver");
        this.el.innerHTML = `

            My Shots

          Select part of the page to save, or save full page without making a selection

        <button class="pageshot-cancel">Cancel</button>
        <button class="pageshot-save">Save Full Page</button>
        `;
        document.body.appendChild(this.el);
        let methods = {
          ".pageshot-myshots": "onMyShots",
          ".pageshot-save": "onSave",
          ".pageshot-cancel": "onCancel"
        };
        Object.keys(methods).forEach((selector) => {
          this.el.querySelector(selector).addEventListener("click", (event) => {
            let result;
            if (this[methods[selector]]) {
              let method = this[methods[selector]];
              result = method.call(this);
            }
            if (result === false) {
              event.preventDefault();
              event.stopPropagation();
              return false;
            }
            return undefined;
          });
        });
        document.body.appendChild(this.el);
      }
    },

    remove: function () {
      util.removeNode(this.el);
      this.el = null;
    },

    el: null
  };

  return exports;
})();
<commit_msg>Temporarily switch handlers back to the parent so that more things work again<commit_after>/* globals util, window, document, console */
/* exported ui */

var isChrome = false;

const ui = (function () { // eslint-disable-line no-unused-vars
  let exports = {};

  // The  tag itself can have margins and offsets, which need to be used when
  // setting the position of the boxEl.
  function getBodyRect() {
    if (getBodyRect.cached) {
      return getBodyRect.cached;
    }
    let rect = document.body.getBoundingClientRect();
    let cached = {
      top: rect.top + window.scrollY,
      bottom: rect.bottom + window.scrollY,
      left: rect.left + window.scrollX,
      right: rect.right + window.scrollX
    };
    // FIXME: I can't decide when this is necessary
    // *not* necessary on http://patriciogonzalezvivo.com/2015/thebookofshaders/
    // (actually causes mis-selection there)
    // *is* necessary on http://atirip.com/2015/03/17/sorry-sad-state-of-matrix-transforms-in-browsers/
    cached = {top: 0, bottom: 0, left: 0, right: 0};
    getBodyRect.cached = cached;
    return cached;
  }

  function isHeader(el) {
    while (el) {
      if (el.className &&
          (el.className.indexOf("pageshot-saver") !== -1 ||
           el.className.indexOf("pageshot-myshots") !== -1 ||
           el.className.indexOf("pageshot-visible") !== -1 ||
           el.className.indexOf("pageshot-full-page") !== -1)) {
        return true;
      }
      el = el.parentNode;
    }
    return false;
  }
  exports.isHeader = isHeader;

  function makeEl(tagName, className) {
    let el = document.createElement(tagName);
    if (className) {
      el.className = className;
    }
    return el;
  }

  /** Represents the shadow overlay that covers the whole page */
  let WholePageOverlay = exports.WholePageOverlay = {

    display: function (callbacks, installHandlersOnDocument) {

      if (! this.overlayFrame) {
        let bodyRect = document.body.getBoundingClientRect();

        this.overlayFrame = document.createElement("iframe");
        // Comment this to get the handlers to work when installed on the iframe.
        this.overlayFrame.style.pointerEvents = "none";
        this.overlayFrame.style.zIndex = "99999999999";
        this.overlayFrame.style.border = "none";
        this.overlayFrame.style.position = "absolute";
        this.overlayFrame.style.top = "0";
        this.overlayFrame.style.left = "0";
        this.overlayFrame.style.height = "100%";
        this.overlayFrame.style.width = "100%";

        document.body.appendChild(this.overlayFrame);

        this.overlayFrame.onload = () => {
          var parsedDom = (new DOMParser()).parseFromString(
            "<title></title>",
            "text/html"
          );
          let frameDoc = this.overlayFrame.contentDocument;

          frameDoc.replaceChild(
              frameDoc.adoptNode(parsedDom.documentElement),
              frameDoc.documentElement
          );

          let overlayEl = makeEl("div", "pageshot-preview-overlay");
          let instructions = makeEl("div", "pageshot-preview-instructions");
          instructions.textContent = "Drag or click on the page to select a region. Press ESC to cancel.";
          overlayEl.appendChild(instructions);
          let button = makeEl("div", "pageshot-myshots");
          button.addEventListener("click", callbacks.onOpenMyShots, false);
          let myShotsPre = makeEl("div", "pageshot-pre-myshots");
          button.appendChild(myShotsPre);
          let text = makeEl("div", "pageshot-myshots-text");
          text.textContent = "My Shots";
          button.appendChild(text);
          let myShotsPost = makeEl("div", "pageshot-post-myshots");
          button.appendChild(myShotsPost);
          overlayEl.appendChild(button);
          let visibleButton = makeEl("div", "pageshot-overlay-button pageshot-visible");
          visibleButton.textContent = "Save visible";
          visibleButton.addEventListener("click", callbacks.onClickVisible, false);
          overlayEl.appendChild(visibleButton);
          let fullPageButton = makeEl("div", "pageshot-overlay-button pageshot-full-page");
          fullPageButton.textContent = "Save full page";
          fullPageButton.addEventListener("click", callbacks.onClickFullPage, false);
          overlayEl.appendChild(fullPageButton);
          frameDoc.body.appendChild(overlayEl);
          let linkUrl = self.options["inline-selection.css"];
          var link = frameDoc.getElementById("pageshot-stylesheet");
          if (! link) {
            link = frameDoc.createElement("link");
            link.setAttribute("rel", "stylesheet");
            link.setAttribute("id", "pageshot-stylesheet");
            link.setAttribute("href", linkUrl);
            frameDoc.head.appendChild(link);
          }
          // Change this to frameDoc to install on the child
          installHandlersOnDocument(document);
        };
      }
    },

    remove: function () {
      util.removeNode(this.overlayFrame);
      this.overlayFrame = null;
    },

    el: null
  };

  let movements = ["topLeft", "top", "topRight", "left", "right", "bottomLeft", "bottom", "bottomRight"];

  /** Creates the selection box */
  exports.Box = {

    display: function (pos, callbacks) {
      this._createEl();
      if (callbacks !== undefined && callbacks.cancel) {
        // We use onclick here because we don't want addEventListener
        // to add multiple event handlers to the same button
        this.cancel.onclick = callbacks.cancel;
        this.cancel.style.display = "";
      } else {
        this.cancel.style.display = "none";
      }
      if (callbacks !== undefined && callbacks.save) {
        // We use onclick here because we don't want addEventListener
        // to add multiple event handlers to the same button
        this.save.removeAttribute("disabled");
        this.save.onclick = (e) => {
          this.save.setAttribute("disabled", "true");
          callbacks.save(e);
        };
        this.save.style.display = "";
      } else {
        this.save.style.display = "none";
      }
      let bodyRect = getBodyRect();
      // Note, document.documentElement.scrollHeight is zero on some strange pages (such as the page created when you load an image):
      let docHeight = Math.max(document.documentElement.scrollHeight || 0, document.body.scrollHeight);
      let docWidth = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth);
      if ((pos.right - pos.left) < 78 || (pos.bottom - pos.top) < 78) {
        this.el.classList.add("pageshot-small-selection");
      } else {
        this.el.classList.remove("pageshot-small-selection");
      }
      this.el.style.top = (pos.top - bodyRect.top) + "px";
      this.el.style.left = (pos.left - bodyRect.left) + "px";
      this.el.style.height = (pos.bottom - pos.top - bodyRect.top) + "px";
      this.el.style.width = (pos.right - pos.left - bodyRect.left) + "px";
      this.bgTop.style.top = "0px";
      this.bgTop.style.height = (pos.top - bodyRect.top) + "px";
      this.bgTop.style.left = "0px";
      this.bgTop.style.width = docWidth + "px";
      this.bgBottom.style.top = (pos.bottom - bodyRect.top) + "px";
      this.bgBottom.style.height = docHeight - (pos.bottom - bodyRect.top) + "px";
      this.bgBottom.style.left = "0px";
      this.bgBottom.style.width = docWidth + "px";
      this.bgLeft.style.top = (pos.top - bodyRect.top) + "px";
      this.bgLeft.style.height = pos.bottom - pos.top  + "px";
      this.bgLeft.style.left = "0px";
      this.bgLeft.style.width = (pos.left - bodyRect.left) + "px";
      this.bgRight.style.top = (pos.top - bodyRect.top) + "px";
      this.bgRight.style.height = pos.bottom - pos.top + "px";
      this.bgRight.style.left = (pos.right - bodyRect.left) + "px";
      this.bgRight.style.width = docWidth - (pos.right - bodyRect.left) + "px";
      WholePageOverlay.remove();
    },

    remove: function () {
      for (let name of ["el", "bgTop", "bgLeft", "bgRight", "bgBottom"]) {
        util.removeNode(this[name]);
        this[name] = null;
      }
    },

    _createEl: function () {
      let boxEl = this.el;
      if (boxEl) {
        return;
      }
      boxEl = makeEl("div", "pageshot-highlight");
      let buttons = makeEl("div", "pageshot-highlight-buttons");
      let cancel = makeEl("button", "pageshot-highlight-button-cancel");
      cancel.textContent = "Cancel";
      buttons.appendChild(cancel);
      let save = makeEl("button", "pageshot-highlight-button-save");
      save.textContent = "Save";
      buttons.appendChild(save);
      this.cancel = cancel;
      this.save = save;
      boxEl.appendChild(buttons);
      for (let name of movements) {
        let elTarget = makeEl("div", "pageshot-mover-target pageshot-" + name);
        let elMover = makeEl("div", "pageshot-mover");
        elTarget.appendChild(elMover);
        boxEl.appendChild(elTarget);
      }
      this.bgTop = makeEl("div", "pageshot-bghighlight");
      document.body.appendChild(this.bgTop);
      this.bgLeft = makeEl("div", "pageshot-bghighlight");
      document.body.appendChild(this.bgLeft);
      this.bgRight = makeEl("div", "pageshot-bghighlight");
      document.body.appendChild(this.bgRight);
      this.bgBottom = makeEl("div", "pageshot-bghighlight");
      document.body.appendChild(this.bgBottom);
      document.body.appendChild(boxEl);
      this.el = boxEl;
    },

    draggerDirection: function (target) {
      while (target) {
        if (target.nodeType == document.ELEMENT_NODE) {
          if (target.classList.contains("pageshot-mover-target")) {
            for (let name of movements) {
              if (target.classList.contains("pageshot-" + name)) {
                return name;
              }
            }
            console.warn("Got pageshot-mover-target that wasn't a specific direction");
          }
        }
        target = target.parentNode;
      }
      return null;
    },

    isSelection: function (target) {
      while (target) {
        if (target.nodeType == document.ELEMENT_NODE && target.classList.contains("pageshot-highlight")) {
          return true;
        }
        target = target.parentNode;
      }
      return false;
    },

    el: null,
    boxTopEl: null,
    boxLeftEl: null,
    boxRightEl: null,
    boxBottomEl: null
  };

  exports.MyShotsReminder = {

    display: function () {
      if (this.dialogEl) {
        return;
      }
      let div = makeEl("div", "pageshot-myshots-reminder");
      if (isChrome) {
        div.className += " pageshot-myshots-reminder-chrome";
      }
      div.innerHTML = `

            Click this button to view all the shots you've taken

      `;
      document.body.appendChild(div);
      this.dialogEl = div;
    },

    remove: function () {
      util.removeNode(this.dialogEl);
      this.dialogEl = null;
    },

    dialogEl: null
  };

  exports.HoverBox = {

    el: null,

    display: function (rect) {
      if (! this.el) {
        this.el = makeEl("div", "pageshot-hover-highlight");
        document.body.appendChild(this.el);
      }
      this.el.style.display = "";
      this.el.style.top = (rect.top - 1) + "px";
      this.el.style.left = (rect.left - 1) + "px";
      this.el.style.width = (rect.right - rect.left + 2) + "px";
      this.el.style.height = (rect.bottom - rect.top + 2) + "px";
    },

    hide: function () {
      if (this.el) {
        this.el.style.display = "none";
      }
    },

    remove: function () {
      util.removeNode(this.el);
      this.el = null;
    }
  };

  /** Removes every UI this module creates */
  exports.remove = function () {
    for (let name in exports) {
      if (typeof exports[name] == "object" && exports[name].remove) {
        exports[name].remove();
      }
    }
  };

  exports.ChromeInterface = {

    onMyShots: null,
    onSave: null,
    onCancel: null,

    display: function () {
      if (! this.el) {
        this.el = makeEl("div", "pageshot-saver");
        this.el.innerHTML = `

            My Shots

          Select part of the page to save, or save full page without making a selection

        <button class="pageshot-cancel">Cancel</button>
        <button class="pageshot-save">Save Full Page</button>
        `;
        document.body.appendChild(this.el);
        let methods = {
          ".pageshot-myshots": "onMyShots",
          ".pageshot-save": "onSave",
          ".pageshot-cancel": "onCancel"
        };
        Object.keys(methods).forEach((selector) => {
          this.el.querySelector(selector).addEventListener("click", (event) => {
            let result;
            if (this[methods[selector]]) {
              let method = this[methods[selector]];
              result = method.call(this);
            }
            if (result === false) {
              event.preventDefault();
              event.stopPropagation();
              return false;
            }
            return undefined;
          });
        });
        document.body.appendChild(this.el);
      }
    },

    remove: function () {
      util.removeNode(this.el);
      this.el = null;
    },

    el: null
  };

  return exports;
})();
<|endoftext|>


<commit_before>// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt

frappe.ui.FilterList = Class.extend({
	init: function(opts) {
		$.extend(this, opts);
		this.filters = [];
		this.wrapper = this.parent;
		this.stats = [];
		this.make();
		this.set_events();
	},
	make: function() {
		this.wrapper.find('.show_filters, .filter_area').remove();
		this.wrapper.append(`

					<button
						style="margin-right: 10px;"
						class="btn btn-default btn-xs new-filter text-muted">
						${__("Add Filter")}</button>

`);
	},
	set_events: function() {
		var me = this;
		// show filters
		this.wrapper.find('.new-filter').bind('click', function() {
			me.add_filter();
		});

		this.wrapper.find('.clear-filters').bind('click', function() {
			me.clear_filters();
			$('.date-range-picker').val('')
			me.base_list.run();
			$(this).addClass("hide");
		});
	},

	show_filters: function() {
		this.wrapper.find('.show_filters').toggle();
		if(!this.filters.length) {
			this.add_filter(this.doctype, 'name');
			this.filters[0].wrapper.find(".filter_field input").focus();
		}
	},

	clear_filters: function() {
		$.each(this.filters, function(i, f) { f.remove(true); });
		if(this.base_list.page.fields_dict) {
			$.each(this.base_list.page.fields_dict, (key, value) => {
				value.set_input('');
			});
		}
		this.filters = [];
	},

	add_filter: function(doctype, fieldname, condition, value, hidden) {
		// adds a new filter, returns true if filter has been added

		// allow equal to be used as like
		let base_filter = this.base_list.page.fields_dict[fieldname];
		if (base_filter
			&& (base_filter.df.condition==condition
				|| (condition==='=' && base_filter.df.condition==='like'))) {
			// if filter exists in base_list, then exit
			this.base_list.page.fields_dict[fieldname].set_input(value);

			return true;
		}

		if(doctype && fieldname
			&& !frappe.meta.has_field(doctype, fieldname)
			&& !in_list(frappe.model.std_fields_list, fieldname)) {
			frappe.msgprint({
				message: __('Filter {0} missing', [fieldname.bold()]),
				title: 'Invalid Filter',
				indicator: 'red'
			});
			return false;
		}

		this.wrapper.find('.show_filters').toggle(true);
		var is_new_filter = arguments.length===0;

		if (is_new_filter && this.wrapper.find(".is-new-filter:visible").length) {
			// only allow 1 new filter at a time!
			return false;
		}

		var filter = this.push_new_filter(doctype, fieldname, condition, value);
		if (!filter) return;

		if(this.wrapper.find('.clear-filters').hasClass("hide")) {
			this.wrapper.find('.clear-filters').removeClass("hide");
		}

		if (filter && is_new_filter) {
			filter.wrapper.addClass("is-new-filter");
		} else {
			filter.freeze();
		}

		if (hidden) {
			filter.$btn_group.addClass("hide");
		}

		return true;
	},
	push_new_filter: function(doctype, fieldname, condition, value) {
		if(this.filter_exists(doctype, fieldname, condition, value)) {
			return;
		}

		// if standard filter exists, then clear it.
		if(this.base_list.page.fields_dict[fieldname]) {
			this.base_list.page.fields_dict[fieldname].set_input('');
		}

		var filter = new frappe.ui.Filter({
			flist: this,
			_doctype: doctype,
			fieldname: fieldname,
			condition: condition,
			value: value
		});

		this.filters.push(filter);

		return filter;
	},

	remove: function(filter) {
		// remove `filter` from flist
		for (var i in this.filters) {
			if (this.filters[i] === filter) {
				break;
			}
		}
		if (i!==undefined) {
			// remove index
			this.filters.splice(i, 1);
		}
	},

	filter_exists: function(doctype, fieldname, condition, value) {
		var flag = false;
		for(var i in this.filters) {
			if(this.filters[i].field) {
				var f = this.filters[i].get_value();

				if(f[0]==doctype && f[1]==fieldname && f[2]==condition && f[3]==value) {
					flag = true;
				} else if($.isArray(value) && frappe.utils.arrays_equal(value, f[3])) {
					flag = true;
				}
			}
		}
		return flag;
	},

	get_filters: function() {
		// get filter values as dict
		var values = [];
		$.each(this.filters, function(i, filter) {
			if(filter.field) {
				filter.freeze();
				values.push(filter.get_value());
			}
		});
		this.base_list.update_standard_filters(values);

		return values;
	},

	// remove hidden filters
	update_filters: function() {
		var fl = [];
		$.each(this.filters, function(i, f) {
			if(f.field) fl.push(f);
		})
		this.filters = fl;
		if(this.filters.length === 0) {
			this.wrapper.find('.clear-filters').addClass("hide");
		}
	},

	get_filter: function(fieldname) {
		for(var i in this.filters) {
			if(this.filters[i].field && this.filters[i].field.df.fieldname==fieldname)
				return this.filters[i];
		}
	},

	get_formatted_value: function(field, val){
		var value = val;

		if(field.df.fieldname==="docstatus") {
			value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value;
		} else if(field.df.original_type==="Check") {
			value = {0:"No", 1:"Yes"}[cint(value)];
		} else if (field.df.original_type === "Duration") {
			let duration_options = {
				hide_days: field.df.hide_days,
				hide_seconds: field.df.hide_seconds
			};
			value = frappe.utils.get_formatted_duration(value, duration_options);
		}

		value = frappe.format(value, field.df, {only_value: 1});
		return value;
	}
});

frappe.ui.Filter = Class.extend({
	init: function(opts) {
		$.extend(this, opts);

		this.doctype = this.flist.doctype;
		this.make();
		this.make_select();
		this.set_events();
	},
	make: function() {
		this.wrapper = $(frappe.render_template("edit_filter", {}))
			.appendTo(this.flist.wrapper.find('.filter_area'));
	},
	make_select: function() {
		var me = this;
		this.fieldselect = new frappe.ui.FieldSelect({
			parent: this.wrapper.find('.fieldname_select_area'),
			doctype: this.doctype,
			filter_fields: this.filter_fields,
			select: function(doctype, fieldname) {
				me.set_field(doctype, fieldname);
			}
		});
		if(this.fieldname) {
			this.fieldselect.set_value(this._doctype || this.doctype, this.fieldname);
		}
	},
	set_events: function() {
		var me = this;

		this.wrapper.find("a.remove-filter").on("click", function() {
			me.remove();
		});

		this.wrapper.find(".set-filter-and-run").on("click", function() {
			me.wrapper.removeClass("is-new-filter");
			me.flist.base_list.run();
			me.apply();
		});

		// add help for "in" codition
		me.wrapper.find('.condition').change(function() {
			if(!me.field) return;
			var condition = $(this).val();
			if(in_list(["in", "like", "not in", "not like"], condition)) {
				me.set_field(me.field.df.parent, me.field.df.fieldname, 'Data', condition);
				if(!me.field.desc_area) {
					me.field.desc_area = $('
').appendTo(me.field.wrapper);
				}
				// set description
				me.field.desc_area.html((in_list(["in", "not in"], condition)==="in"
					? __("values separated by commas")
					: __("use % as wildcard"))+'
');
			} else {
				//if condition selected after refresh
				me.set_field(me.field.df.parent, me.field.df.fieldname, null, condition);
			}
		});

		// set the field
		if(me.fieldname) {
			// pre-sets given (could be via tags!)
			return this.set_values(me._doctype, me.fieldname, me.condition, me.value);
		} else {
			me.set_field(me.doctype, 'name');
		}
	},

	apply: function() {
		var f = this.get_value();

		this.flist.remove(this);
		this.flist.push_new_filter(f[0], f[1], f[2], f[3]);
		this.remove();
	},

	remove: function(dont_run) {
		this.wrapper.remove();
		this.$btn_group && this.$btn_group.remove();
		this.field = null;
		this.flist.update_filters();

		if(!dont_run) {
			this.flist.base_list.refresh(true);
		}
	},

	set_values: function(doctype, fieldname, condition, value) {
		// presents given (could be via tags!)
		this.set_field(doctype, fieldname);

		// change 0,1 to Yes, No for check field type
		if(this.field.df.original_type==='Check') {
			if(value==0) value = 'No';
			else if(value==1) value = 'Yes';
		}

		if(condition) {
			this.wrapper.find('.condition').val(condition).change();
		}
		if(value!=null) {
			return this.field.set_value(value);
		}
	},

	set_field: function(doctype, fieldname, fieldtype, condition) {
		var me = this;

		// set in fieldname (again)
		var cur = me.field ? {
			fieldname: me.field.df.fieldname,
			fieldtype: me.field.df.fieldtype,
			parent: me.field.df.parent,
		} : {};

		var original_docfield = me.fieldselect.fields_by_name[doctype][fieldname];
		if(!original_docfield) {
			frappe.msgprint(__("Field {0} is not selectable.", [fieldname]));
			return;
		}

		var df = copy_dict(me.fieldselect.fields_by_name[doctype][fieldname]);

		// filter field shouldn't be read only or hidden
		df.read_only = 0;
		df.hidden = 0;

		if(!condition) this.set_default_condition(df, fieldtype);
		this.set_fieldtype(df, fieldtype);

		// called when condition is changed,
		// don't change if all is well
		if(me.field && cur.fieldname == fieldname && df.fieldtype == cur.fieldtype &&
			df.parent == cur.parent) {
			return;
		}

		// clear field area and make field
		me.fieldselect.selected_doctype = doctype;
		me.fieldselect.selected_fieldname = fieldname;

		// save old text
		var old_text = null;
		if(me.field) {
			old_text = me.field.get_value();
		}

		var field_area = me.wrapper.find('.filter_field').empty().get(0);
		var f = frappe.ui.form.make_control({
			df: df,
			parent: field_area,
			only_input: true,
		})
		f.refresh();

		me.field = f;
		if(old_text && me.field.df.fieldtype===cur.fieldtype) {
			me.field.set_value(old_text);
		}

		// run on enter
		$(me.field.wrapper).find(':input').keydown(function(ev) {
			if(ev.which==13) {
				me.flist.base_list.run();
			}
		})
	},

	set_fieldtype: function(df, fieldtype) {
		// reset
		if(df.original_type)
			df.fieldtype = df.original_type;
		else
			df.original_type = df.fieldtype;

		df.description = ''; df.reqd = 0;
		df.ignore_link_validation = true;

		// given
		if(fieldtype) {
			df.fieldtype = fieldtype;
			return;
		}

		// scrub
		if(df.fieldname=="docstatus") {
			df.fieldtype="Select",
			df.options=[
				{value:0, label:__("Draft")},
				{value:1, label:__("Submitted")},
				{value:2, label:__("Cancelled")}
			]
		} else if(df.fieldtype=='Check') {
			df.fieldtype='Select';
			df.options='No\nYes';
		} else if(['Text','Small Text','Text Editor','Code','Tag','Comments',
			'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) {
			df.fieldtype = 'Data';
		} else if(df.fieldtype=='Link' && ['=', '!='].indexOf(this.wrapper.find('.condition').val())==-1) {
			df.fieldtype = 'Data';
		}
		if(df.fieldtype==="Data" && (df.options || "").toLowerCase()==="email") {
			df.options = null;
		}
		if(this.wrapper.find('.condition').val()== "Between" && (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')){
			df.fieldtype = 'DateRange';
		}
	},

	set_default_condition: function(df, fieldtype) {
		if(!fieldtype) {
			// set as "like" for data fields
			if (df.fieldtype == 'Data') {
				this.wrapper.find('.condition').val('like');
			} else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime'){
				this.wrapper.find('.condition').val('Between');
			}else{
				this.wrapper.find('.condition').val('=');
			}
		}
	},

	get_value: function() {
		return [this.fieldselect.selected_doctype,
			this.field.df.fieldname, this.get_condition(), this.get_selected_value()];
	},

	get_selected_value: function() {
		var val = this.field.get_value();

		if(typeof val==='string') {
			val = strip(val);
		}

		if(this.field.df.original_type == 'Check') {
			val = (val=='Yes' ? 1 :0);
		}

		if(this.get_condition().indexOf('like', 'not like')!==-1) {
			// automatically append wildcards
			if(val) {
				if(val.slice(0,1) !== "%") {
					val = "%" + val;
				}
				if(val.slice(-1) !== "%") {
					val = val + "%";
				}
			}
		} else if(in_list(["in", "not in"], this.get_condition())) {
			if(val) {
				val = $.map(val.split(","), function(v) { return strip(v); });
			}
		} if(val === '%') {
			val = "";
		}

		return val;
	},

	get_condition: function() {
		return this.wrapper.find('.condition').val();
	},

	freeze: function() {
		if(this.$btn_group) {
			// already made, just hide the condition setter
			this.set_filter_button_text();
			this.wrapper.toggle(false);
			return;
		}

		var me = this;

		// add a button for new filter if missing
		this.$btn_group = $(`

			<button class="btn btn-default btn-xs toggle-filter"
				title="${ __("Edit Filter") }">
			</button>
			<button class="btn btn-default btn-xs remove-filter"
				title="${ __("Remove Filter") }">

			</button>
`)
			.insertAfter(this.flist.wrapper.find(".set-filters .new-filter"));

		this.set_filter_button_text();

		this.$btn_group.find(".remove-filter").on("click", function() {
			me.remove();
		});

		this.$btn_group.find(".toggle-filter").on("click", function() {
			$(this).closest('.show_filters').find('.filter_area').show();
			me.wrapper.toggle();
		})
		this.wrapper.toggle(false);
	},

	set_filter_button_text: function() {
		var value = this.get_selected_value();
		value = this.flist.get_formatted_value(this.field, value);

		// for translations
		// __("like"), __("not like"), __("in")

		this.$btn_group.find(".toggle-filter")
			.html(repl('%(label)s %(condition)s "%(value)s"', {
				label: __(this.field.df.label),
				condition: __(this.get_condition()),
				value: __(value),
			}));
	}

});

// <select> widget with all fields of a doctype as options
frappe.ui.FieldSelect = Class.extend({
	// opts parent, doctype, filter_fields, with_blank, select
	init: function(opts) {
		var me = this;
		$.extend(this, opts);
		this.fields_by_name = {};
		this.options = [];
		this.$select = $('<input class="form-control">')
			.appendTo(this.parent)
			.on("click", function () { $(this).select(); });
		this.select_input = this.$select.get(0);
		this.awesomplete = new Awesomplete(this.select_input, {
			minChars: 0,
			maxItems: 99,
			autoFirst: true,
			list: me.options,
			item: function(item, input) {
				return $(repl('

%(label)s

', item))
				.data("item.autocomplete", item)
				.get(0);
			}
		});
		this.$select.on("awesomplete-select", function(e) {
			var o = e.originalEvent;
			var value = o.text.value;
			var item = me.awesomplete.get_item(value);
			me.selected_doctype = item.doctype;
			me.selected_fieldname = item.fieldname;
			if(me.select) me.select(item.doctype, item.fieldname);
		});
		this.$select.on("awesomplete-selectcomplete", function(e) {
			var o = e.originalEvent;
			var value = o.text.value;
			var item = me.awesomplete.get_item(value);
			me.$select.val(item.label);
		});

		if(this.filter_fields) {
			for(var i in this.filter_fields)
				this.add_field_option(this.filter_fields[i])
		} else {
			this.build_options();
		}
		this.set_value(this.doctype, "name");
		window.last_filter = this;
	},
	get_value: function() {
		return this.selected_doctype ? this.selected_doctype + "." + this.selected_fieldname : null;
	},
	val: function(value) {
		if(value===undefined) {
			return this.get_value();
		} else {
			this.set_value(value);
		}
	},
	clear: function() {
		this.selected_doctype = null;
		this.selected_fieldname = null;
		this.$select.val("");
	},
	set_value: function(doctype, fieldname) {
		var me = this;
		this.clear();
		if(!doctype) return;

		// old style
		if(doctype.indexOf(".")!==-1) {
			var parts = doctype.split(".");
			doctype = parts[0];
			fieldname = parts[1];
		}

		$.each(this.options, function(i, v) {
			if(v.doctype===doctype && v.fieldname===fieldname) {
				me.selected_doctype = doctype;
				me.selected_fieldname = fieldname;
				me.$select.val(v.label);
				return false;
			}
		});
	},
	build_options: function() {
		var me = this;
		me.table_fields = [];
		var std_filters = $.map(frappe.model.std_fields, function(d) {
			var opts = {parent: me.doctype}
			if(d.fieldname=="name") opts.options = me.doctype;
			return $.extend(copy_dict(d), opts);
		});

		// add parenttype column
		var doctype_obj = locals['DocType'][me.doctype];
		if(doctype_obj && cint(doctype_obj.istable)) {
			std_filters = std_filters.concat([{
				fieldname: 'parent',
				fieldtype: 'Data',
				label: 'Parent',
				parent: me.doctype,
			}]);
		}

		// blank
		if(this.with_blank) {
			this.options.push({
				label:"",
				value:"",
			})
		}

		// main table
		var main_table_fields = std_filters.concat(frappe.meta.docfield_list[me.doctype]);
		$.each(frappe.utils.sort(main_table_fields, "label", "string"), function(i, df) {
			// show fields where user has read access and if report hide flag is not set
			if(frappe.perm.has_perm(me.doctype, df.permlevel, "read") && !df.report_hide)
				me.add_field_option(df);
		});

		// child tables
		$.each(me.table_fields, function(i, table_df) {
			if(table_df.options) {
				var child_table_fields = [].concat(frappe.meta.docfield_list[table_df.options]);
				$.each(frappe.utils.sort(child_table_fields, "label", "string"), function(i, df) {
					// show fields where user has read access and if report hide flag is not set
					if(frappe.perm.has_perm(me.doctype, df.permlevel, "read") && !df.report_hide)
						me.add_field_option(df);
				});
			}
		});
	},

	add_field_option: function(df) {
		var me = this;
		if(me.doctype && df.parent==me.doctype) {
			var label = __(df.label);
			var table = me.doctype;
			if(frappe.model.table_fields.includes(df.fieldtype)) me.table_fields.push(df);
		} else {
			var label = __(df.label) + ' (' + __(df.parent) + ')';
			var table = df.parent;
		}
		if(frappe.model.no_value_type.indexOf(df.fieldtype) == -1 &&
			!(me.fields_by_name[df.parent] && me.fields_by_name[df.parent][df.fieldname])) {
			this.options.push({
				label: label,
				value: table + "." + df.fieldname,
				fieldname: df.fieldname,
				doctype: df.parent
			});
			if(!me.fields_by_name[df.parent]) me.fields_by_name[df.parent] = {};
			me.fields_by_name[df.parent][df.fieldname] = df;
		}
	},
})
<commit_msg>refactor: delete unused file filters.js<commit_after><|endoftext|>


<commit_before>(function() {
  'use strict';

  var app = Marionette.Application.extend({});
  app = new app();

  var MyObject = Marionette.Object.extend({

    initialize: function(options) {
      console.log("LOG: Object got initialize");
      console.log("LOG: options object: ", options);
    }

  });

  new MyObject({
    "name": "Ashwin Hegde",
    "job": "Software Engineer"
  });

  app.start();

})();
<commit_msg>Updates object eg<commit_after>(function() {
  'use strict';

  var app = Marionette.Application.extend({});
  app = new app();

  var MyObject = Marionette.Object.extend({

    initialize: function(options) {
      console.log("LOG: Object got initialize");
      console.log("LOG: (options) object - ", options);

      this.fetchObjOpt();
    },

    fetchObjOpt: function() {
      console.log("LOG: (this.options) object - ", this.options);
    }

  });

  new MyObject({
    "name": "Ashwin Hegde",
    "job": "Software Engineer"
  });

  app.start();

})();
<|endoftext|>


<commit_before>var NormalDistribution = require('./NormalDistribution');
var random = require('seeded-random');

/*
 * Class which generates a football match score.
 */
function Match(teamA, teamB, options) {
    options = options || {};

    this.team = [teamA, teamB];
    this.winner;
    this.ratingDifference = teamA.rating - teamB.rating;

    this.mean = 1.58;
    this.sd = 1.23;
    this.constant = 0.0025;
    this.extraTimeEnabled = options.extraTime || false;
    this.penaltiesEnabled = options.penalties || false;
    this.seed = options.seed || Math.random() + '';
    this.goals = [];
    this.penalties = [];
    this.goalTimes = [[], []];
    this.result;
    this.text;

    this.graph = [
        new NormalDistribution(this.mean + (this.ratingDifference * this.constant), this.sd),
        new NormalDistribution(this.mean - (this.ratingDifference * this.constant), this.sd)
    ];
}

(function (static_, proto_) {

    /*
     * Generates a match score from the given seed.
     */
    proto_.simulate = function () {
        this.goals[0] = this._goalsScored(0);
        this.goals[1] = this._goalsScored(1);
        this.penalties = [null, null];
        var extraTime = false;

        this._generateGoalTimes();

        // If the match is a draw and extra time is enabled.
        if (this.extraTimeEnabled && this.goals[0] === this.goals[1]) {
            extraTime = true;
            this.goals[0] += this._goalsScored(0, 30);
            this.goals[1] += this._goalsScored(1, 30);
        }

        // Calculate result. 1 = win, 0.5 = draw,
        // 0 = loss.
        if (this.goals[0] > this.goals[1]) {
            this.result = 0;
        }
        else if (this.goals[0] === this.goals[1]) {
            this.result = 0.5;
        }
        else {
            this.result = 1;
        }

        // If it's still and draw and penalties are enabled.
        if (this.penaltiesEnabled && this.goals[0] === this.goals[1]) {
            this.penalties = this._penalties();
        }

        if (this.penalties[0] > this.penalties[1]) {
            this.result = 0;
        }
        else if (this.penalties[1] > this.penalties[0]) {
            this.result = 1;
        }

        this.winner = this._getWinner();

        // Create the output text
        this.text = this._outputText(extraTime);

        return {
            score: this.goals,
            penalties: this.penalties,
            text: this.text,
            result: this.result
        };
    };

    /*
     * Finds the number of goals scored by a team.
     */
    proto_._goalsScored = function (team, time) {
        rand = random.decimal(this.seed + ' ' + team + ' ' + time);
        time = time || 90;

        var graph = this.graph[team];
        var goals = 0;
        var xValue = -10;
        var mean = graph.mean;
        var standardDeviation = graph.standardDeviation;

        graph.mean = mean * (time / 90);
        graph.standardDeviation = standardDeviation * (time / 90);

        while (rand > graph.trapeziumRule(xValue, goals + 0.5, 0.1)) {
            goals += 1;
        }

        graph.mean = mean;
        graph.standardDeviation = standardDeviation;

        return goals;
    };

    /*
     * Gets winner.
     */
    proto_._getWinner = function () {
        if (this.result !== 0.5) {
            return this.team[this.result];
        }

        return null;
    };

    /*
     * Method which generates the output string.
     */
    proto_._outputText = function (extraTime) {
        var text = this.team[0].stringName + ' ' + this.goals[0] + '-' + this.goals[1] + ' ' + this.team[1].stringName;

        if (extraTime) {
            text += ' (aet)';
        }

        if (this.penalties[0] !== null) {
            text += ' (' + this.penalties[0] + '-' + this.penalties[1] + ')';
        }

        return text;
    };

    /*
     * Penalty shootout simulation.
     */
    proto_._penalties = function () {
        var goals = [0, 0];
        var turns = [5, 5];

        while (true) {
            goals[0] += this._takePenalty();
            turns[0] -= 1;

            if (this._penaltiesWon(goals, turns)) {
                break;
            }

            goals[1] += this._takePenalty();
            turns[1] -= 1;

            if (this._penaltiesWon(goals, turns)) {
                break;
            }

            if (turns[0] === 0) {
                turns[0] = 1;
                turns[1] = 1;
            }
        }

        return goals;
    };

    /*
     * Checks to see if the penalty shootout has
     * ended.
     */
    proto_._penaltiesWon = function (goals, turns) {
        if (goals[0] - goals[1] > turns[1] || goals[1] - goals[0] > turns[0]) {
            return true;
        }

        return false;
    };

    /*
     * Gets a penalty goal.
     */
    proto_._takePenalty = function (rand) {
        rand = rand || Math.random();

        var threshold = 0.75;

        if (rand < threshold) {
            return 1;
        }

        return 0;
    };

    /*
     * Generates the times the goals were scored.
     */
    proto_._generateGoalTimes = function () {
        this.goals.forEach(function (goals, index) {
            var seed = this.seed + ' ' + goals + ' ' + index;

            for (var i = 0; i < goals; i += 1) {
                this.goalTimes[index].push(random.range(seed + ' ' + i, 0, 90));
            }
        }.bind(this));
    }

}(Match, Match.prototype));

module.exports = Match;
<commit_msg>Adds extra time goal times<commit_after>var NormalDistribution = require('./NormalDistribution');
var random = require('seeded-random');

/*
 * Class which generates a football match score.
 */
function Match(teamA, teamB, options) {
    options = options || {};

    this.team = [teamA, teamB];
    this.winner;
    this.ratingDifference = teamA.rating - teamB.rating;

    this.mean = 1.58;
    this.sd = 1.23;
    this.constant = 0.0025;
    this.extraTimeEnabled = options.extraTime || false;
    this.penaltiesEnabled = options.penalties || false;
    this.seed = options.seed || Math.random() + '';
    this.goals = [];
    this.goalsExtraTime = [];
    this.penalties = [];
    this.goalTimes = [[], []];
    this.result;
    this.text;

    this.graph = [
        new NormalDistribution(this.mean + (this.ratingDifference * this.constant), this.sd),
        new NormalDistribution(this.mean - (this.ratingDifference * this.constant), this.sd)
    ];
}

(function (static_, proto_) {

    /*
     * Generates a match score from the given seed.
     */
    proto_.simulate = function () {
        this.goals[0] = this._goalsScored(0);
        this.goals[1] = this._goalsScored(1);
        this.penalties = [null, null];
        var extraTime = false;

        this._generateGoalTimes();

        // If the match is a draw and extra time is enabled.
        if (this.extraTimeEnabled && this.goals[0] === this.goals[1]) {
            extraTime = true;
            this.goalsExtraTime[0] = this._goalsScored(0, 30);
            this.goalsExtraTime[1] = this._goalsScored(0, 30);
            this.goals[0] += this.goalsExtraTime[0];
            this.goals[1] += this.goalsExtraTime[1];

            this._generateGoalTimes(91, 120, this.goalsExtraTime);
        }

        // Calculate result. 1 = win, 0.5 = draw,
        // 0 = loss.
        if (this.goals[0] > this.goals[1]) {
            this.result = 0;
        }
        else if (this.goals[0] === this.goals[1]) {
            this.result = 0.5;
        }
        else {
            this.result = 1;
        }

        // If it's still and draw and penalties are enabled.
        if (this.penaltiesEnabled && this.goals[0] === this.goals[1]) {
            this.penalties = this._penalties();
        }

        if (this.penalties[0] > this.penalties[1]) {
            this.result = 0;
        }
        else if (this.penalties[1] > this.penalties[0]) {
            this.result = 1;
        }

        this.winner = this._getWinner();

        // Create the output text
        this.text = this._outputText(extraTime);

        return {
            score: this.goals,
            penalties: this.penalties,
            text: this.text,
            result: this.result
        };
    };

    /*
     * Finds the number of goals scored by a team.
     */
    proto_._goalsScored = function (team, time) {
        rand = random.decimal(this.seed + ' ' + team + ' ' + time);
        time = time || 90;

        var graph = this.graph[team];
        var goals = 0;
        var xValue = -10;
        var mean = graph.mean;
        var standardDeviation = graph.standardDeviation;

        graph.mean = mean * (time / 90);
        graph.standardDeviation = standardDeviation * (time / 90);

        while (rand > graph.trapeziumRule(xValue, goals + 0.5, 0.1)) {
            goals += 1;
        }

        graph.mean = mean;
        graph.standardDeviation = standardDeviation;

        return goals;
    };

    /*
     * Gets winner.
     */
    proto_._getWinner = function () {
        if (this.result !== 0.5) {
            return this.team[this.result];
        }

        return null;
    };

    /*
     * Method which generates the output string.
     */
    proto_._outputText = function (extraTime) {
        var text = this.team[0].stringName + ' ' + this.goals[0] + '-' + this.goals[1] + ' ' + this.team[1].stringName;

        if (extraTime) {
            text += ' (aet)';
        }

        if (this.penalties[0] !== null) {
            text += ' (' + this.penalties[0] + '-' + this.penalties[1] + ')';
        }

        return text;
    };

    /*
     * Penalty shootout simulation.
     */
    proto_._penalties = function () {
        var goals = [0, 0];
        var turns = [5, 5];

        while (true) {
            goals[0] += this._takePenalty();
            turns[0] -= 1;

            if (this._penaltiesWon(goals, turns)) {
                break;
            }

            goals[1] += this._takePenalty();
            turns[1] -= 1;

            if (this._penaltiesWon(goals, turns)) {
                break;
            }

            if (turns[0] === 0) {
                turns[0] = 1;
                turns[1] = 1;
            }
        }

        return goals;
    };

    /*
     * Checks to see if the penalty shootout has
     * ended.
     */
    proto_._penaltiesWon = function (goals, turns) {
        if (goals[0] - goals[1] > turns[1] || goals[1] - goals[0] > turns[0]) {
            return true;
        }

        return false;
    };

    /*
     * Gets a penalty goal.
     */
    proto_._takePenalty = function (rand) {
        rand = rand || Math.random();

        var threshold = 0.75;

        if (rand < threshold) {
            return 1;
        }

        return 0;
    };

    /*
     * Generates the times the goals were scored.
     */
    proto_._generateGoalTimes = function (startTime, endTime, goalArray) {
        startTime = startTime || 1;
        endTime = endTime || 90;
        goalArray = goalArray || this.goals;

        goalArray.forEach(function (goals, index) {
            var seed = this.seed + ' ' + goals + ' ' + index + ' ' + startTime + ' ' + endTime;

            for (var i = 0; i < goals; i += 1) {
                this.goalTimes[index].push(random.range(seed + ' ' + i, startTime, endTime));
            }
        }.bind(this));
    }

}(Match, Match.prototype));

module.exports = Match;
<|endoftext|>