<commit_before>// @flow

import moment from 'moment'
import React, {Component} from 'react'
import {
  Alert,
  Button,
  ButtonToolbar,
  Col,
  Glyphicon,
  Panel,
  Row,
  Table
} from 'react-bootstrap'
import {browserHistory, Link} from 'react-router'

import * as gtfsPlusActions from '../actions/gtfsplus'
import {getGtfsPlusSpec} from '../../common/util/config'
import type {Props as ContainerProps} from '../containers/ActiveGtfsPlusVersionSummary'
import type {GtfsPlusReducerState, ManagerUserState} from '../../types/reducers'

type Issue = {
  description: string,
  fieldName: string,
  rowIndex: number,
  tableId: string
}

type Props = ContainerProps & {
  deleteGtfsPlusFeed: typeof gtfsPlusActions.deleteGtfsPlusFeed,
  downloadGtfsPlusFeed: typeof gtfsPlusActions.downloadGtfsPlusFeed,
  gtfsplus: GtfsPlusReducerState,
  issuesForTable: any,
  publishGtfsPlusFeed: typeof gtfsPlusActions.publishGtfsPlusFeed,
  user: ManagerUserState
}

type State = {
  expanded: boolean
}

export default class GtfsPlusVersionSummary extends Component<Props, State> {
  state = { expanded: false }

  componentDidMount () {
    this.props.downloadGtfsPlusFeed(this.props.version.id)
  }

  isTableIncluded (tableId: string) {
    const {tableData} = this.props.gtfsplus
    if (!tableData) return null
    return tableId in tableData ? 'Yes' : 'No'
  }

  tableRecordCount (tableId: string): ?string {
    const {tableData} = this.props.gtfsplus
    if (!tableData) return null
    if (!(tableId in tableData)) return 'N/A'
    return tableData[tableId].length.toLocaleString()
  }

  validationIssueCount (tableId: string): ?string {
    const {issuesForTable} = this.props
    if (!issuesForTable) return null
    if (!(tableId in issuesForTable)) return 'None'
    return issuesForTable[tableId].length.toLocaleString()
  }

  _getTableLevelIssues = (tableId: string): ?Array<Issue> => {
    const {issuesForTable} = this.props
    if (!issuesForTable) return null
    if (!(tableId in issuesForTable)) return null
    // Table level issues are identified by not having -1 for row index.
    const tableLevelIssues = issuesForTable[tableId].filter(issue => issue.rowIndex === -1)
    return tableLevelIssues.length > 0 ? tableLevelIssues : null
  }

  feedStatus () {
    const {timestamp} = this.props.gtfsplus
    if (!timestamp) return null
    if (!this._gtfsPlusIsEdited()) return GTFS+ data for this feed version has not been edited.
    const timestampMoment = moment(timestamp)
    const formatted = timestampMoment.format('MMM. DD, YYYY, h:MMa')
    const fromNow = timestampMoment.fromNow()
    return GTFS+ data updated {fromNow}.
  }

  _gtfsPlusIsEdited = () => (this.props.gtfsplus.timestamp !== this.props.version.fileTimestamp)

  _onClickEdit = () => {
    const {version} = this.props
    browserHistory.push(`/gtfsplus/${version.feedSource.id}/${version.id}`)
  }

  _onClickDelete = () => {
    if (window.confirm('Are you sure you want to delete the GTFS+ edits for this feed version? Note: this will not delete the feed version itself.')) {
      this.props.deleteGtfsPlusFeed(this.props.version.id)
    }
  }

  _onClickPublish = () => {
    this.props.publishGtfsPlusFeed(this.props.version)
    this.setState({ expanded: false })
  }

  _toggleExpanded = () => {
    const {downloadGtfsPlusFeed, version} = this.props
    const {expanded} = this.state
    if (!expanded) downloadGtfsPlusFeed(version.id)
    this.setState({ expanded: !expanded })
  }

  render () {
    const {
      gtfsplus,
      user,
      version,
      versions
    } = this.props
    const { feedSource } = version
    const editingIsDisabled = !user.permissions ||
      !user.permissions.hasFeedPermission(
        feedSource.organizationId,
        feedSource.projectId,
        feedSource.id,
        'edit-gtfs'
      )
    const publishingIsDisabled = !user.permissions ||
      !user.permissions.hasFeedPermission(
        feedSource.organizationId,
        feedSource.projectId,
        feedSource.id,
        'approve-gtfs'
      )
    const header = (

        <Glyphicon glyph='check' /> GTFS+ for this Version

    )

    const derivedVersions = versions.filter(v => v.originNamespace === version.namespace)
    return (
      <Panel header={header}>
        <Row>
          <Col xs={6}>
            <Alert>
              {this.feedStatus()}
              {derivedVersions.length > 0
                ?

                  Versions created from this version of GTFS+:

                    {derivedVersions.map(v => {
                      const url = `/feed/${v.feedSource.id}/version/${v.version}`
                      return (

                          <Link to={url}>
                            {v.name} (version {v.version})
                          </Link>

                      )
                    })}

                : this._gtfsPlusIsEdited() && gtfsplus.validation && gtfsplus.validation.issues.length === 0
                  ?
Note: Edits must be published as a new feed version.

                  : null

              }
            </Alert>
          </Col>
          <Col xs={6} style={{ textAlign: 'right' }}>
            <ButtonToolbar className='pull-right'>
              {this._gtfsPlusIsEdited()
                ? <Button
                  onClick={this._onClickDelete}>
                  <Glyphicon glyph='remove' /> Clear edits
                </Button>
                : null
              }
              <Button
                disabled={editingIsDisabled}
                bsStyle='primary'
                onClick={this._onClickEdit}>
                <Glyphicon glyph='edit' /> Edit GTFS+
              </Button>
              {this._gtfsPlusIsEdited()
                ? <Button
                  disabled={publishingIsDisabled}
                  bsStyle='primary'
                  onClick={this._onClickPublish}>
                  <Glyphicon glyph='upload' /> Create a New Version
                </Button>
                : null
              }
            </ButtonToolbar>
          </Col>
        </Row>
        <Row>
          <Col xs={12}>
            <Panel>

                    Table
                    Included?
                    Records
                    Validation Issues

                  {getGtfsPlusSpec()
                    .sort((table1, table2) => table1.name.localeCompare(table2.name))
                    .map((table, index) => {
                      const issueCount = this.validationIssueCount(table.id)
                      const tableLevelIssues = this._getTableLevelIssues(table.id)

                      return (

 0 && 'warning'
                          }
                          style={{ color: this.isTableIncluded(table.id) === 'Yes' ? 'black' : 'lightGray' }}>

                            {table.name}
                            {tableLevelIssues
                              ?

                                {tableLevelIssues.length} critical table issue(s):

                                  {tableLevelIssues.map((issue, i) =>

{issue.fieldName}: {issue.description}
)}

                              : null
                            }

                          {this.isTableIncluded(table.id)}
                          {this.tableRecordCount(table.id)}
                          {issueCount}

                      )
                    })}

            </Panel>
          </Col>
        </Row>
      </Panel>
    )
  }
}
<commit_msg>fix(GtfsPlusVersionSummary): Address PR comments<commit_after>// @flow

import moment from 'moment'
import React, {Component} from 'react'
import {
  Alert,
  Button,
  ButtonToolbar,
  Col,
  Glyphicon,
  Panel,
  Row,
  Table
} from 'react-bootstrap'
import {browserHistory, Link} from 'react-router'

import * as gtfsPlusActions from '../actions/gtfsplus'

import {getGtfsPlusSpec} from '../../common/util/config'
import type {Props as ContainerProps} from '../containers/ActiveGtfsPlusVersionSummary'
import type {GtfsPlusValidationIssue} from '../../types'
import type {GtfsPlusReducerState, ManagerUserState} from '../../types/reducers'

type Props = ContainerProps & {
  deleteGtfsPlusFeed: typeof gtfsPlusActions.deleteGtfsPlusFeed,
  downloadGtfsPlusFeed: typeof gtfsPlusActions.downloadGtfsPlusFeed,
  gtfsplus: GtfsPlusReducerState,
  issuesForTable: any,
  publishGtfsPlusFeed: typeof gtfsPlusActions.publishGtfsPlusFeed,
  user: ManagerUserState
}

type State = {
  expanded: boolean
}

export default class GtfsPlusVersionSummary extends Component<Props, State> {
  state = { expanded: false }

  componentDidMount () {
    this.props.downloadGtfsPlusFeed(this.props.version.id)
  }

  isTableIncluded (tableId: string) {
    const {tableData} = this.props.gtfsplus
    if (!tableData) return null
    return tableId in tableData ? 'Yes' : 'No'
  }

  tableRecordCount (tableId: string): ?string {
    const {tableData} = this.props.gtfsplus
    if (!tableData) return null
    if (!(tableId in tableData)) return 'N/A'
    return tableData[tableId].length.toLocaleString()
  }

  validationIssueCount (tableId: string): ?string {
    const {issuesForTable} = this.props
    if (!issuesForTable) return null
    if (!(tableId in issuesForTable)) return 'None'
    return issuesForTable[tableId].length.toLocaleString()
  }

  _getTableLevelIssues = (tableId: string): ?Array<GtfsPlusValidationIssue> => {
    const {issuesForTable} = this.props
    if (!issuesForTable) return null
    if (!(tableId in issuesForTable)) return null
    // Table level issues are identified by not having -1 for row index.
    const tableLevelIssues = issuesForTable[tableId].filter(issue => issue.rowIndex === -1)
    return tableLevelIssues.length > 0 ? tableLevelIssues : null
  }

  feedStatus () {
    const {timestamp} = this.props.gtfsplus
    if (!timestamp) return null
    if (!this._gtfsPlusIsEdited()) return GTFS+ data for this feed version has not been edited.
    const timestampMoment = moment(timestamp)
    const formatted = timestampMoment.format('MMM. DD, YYYY, h:MMa')
    const fromNow = timestampMoment.fromNow()
    return GTFS+ data updated {fromNow}.
  }

  _gtfsPlusIsEdited = () => (this.props.gtfsplus.timestamp !== this.props.version.fileTimestamp)

  _onClickEdit = () => {
    const {version} = this.props
    browserHistory.push(`/gtfsplus/${version.feedSource.id}/${version.id}`)
  }

  _onClickDelete = () => {
    if (window.confirm('Are you sure you want to delete the GTFS+ edits for this feed version? Note: this will not delete the feed version itself.')) {
      this.props.deleteGtfsPlusFeed(this.props.version.id)
    }
  }

  _onClickPublish = () => {
    this.props.publishGtfsPlusFeed(this.props.version)
    this.setState({ expanded: false })
  }

  _toggleExpanded = () => {
    const {downloadGtfsPlusFeed, version} = this.props
    const {expanded} = this.state
    if (!expanded) downloadGtfsPlusFeed(version.id)
    this.setState({ expanded: !expanded })
  }

  render () {
    const {
      gtfsplus,
      user,
      version,
      versions
    } = this.props
    const { feedSource } = version
    const editingIsDisabled = !user.permissions ||
      !user.permissions.hasFeedPermission(
        feedSource.organizationId,
        feedSource.projectId,
        feedSource.id,
        'edit-gtfs'
      )
    const publishingIsDisabled = !user.permissions ||
      !user.permissions.hasFeedPermission(
        feedSource.organizationId,
        feedSource.projectId,
        feedSource.id,
        'approve-gtfs'
      )
    const header = (

        <Glyphicon glyph='check' /> GTFS+ for this Version

    )

    const derivedVersions = versions.filter(v => v.originNamespace === version.namespace)
    return (
      <Panel header={header}>
        <Row>
          <Col xs={6}>
            <Alert>
              {this.feedStatus()}
              {derivedVersions.length > 0
                ?

                  Versions created from this version of GTFS+:

                    {derivedVersions.map(v => {
                      const url = `/feed/${v.feedSource.id}/version/${v.version}`
                      return (

                          <Link to={url}>
                            {v.name} (version {v.version})
                          </Link>

                      )
                    })}

                : this._gtfsPlusIsEdited() && gtfsplus.validation && gtfsplus.validation.issues.length === 0
                  ?
Note: Edits must be published as a new feed version.

                  : null

              }
            </Alert>
          </Col>
          <Col xs={6} style={{ textAlign: 'right' }}>
            <ButtonToolbar className='pull-right'>
              {this._gtfsPlusIsEdited()
                ? <Button
                  onClick={this._onClickDelete}>
                  <Glyphicon glyph='remove' /> Clear edits
                </Button>
                : null
              }
              <Button
                disabled={editingIsDisabled}
                bsStyle='primary'
                onClick={this._onClickEdit}>
                <Glyphicon glyph='edit' /> Edit GTFS+
              </Button>
              {this._gtfsPlusIsEdited()
                ? <Button
                  disabled={publishingIsDisabled}
                  bsStyle='primary'
                  onClick={this._onClickPublish}>
                  <Glyphicon glyph='upload' /> Create a New Version
                </Button>
                : null
              }
            </ButtonToolbar>
          </Col>
        </Row>
        <Row>
          <Col xs={12}>
            <Panel>

                    Table
                    Included?
                    Records
                    Validation Issues

                  {getGtfsPlusSpec()
                    .sort((table1, table2) => table1.name.localeCompare(table2.name))
                    .map((table, index) => {
                      const issueCount = this.validationIssueCount(table.id)
                      const tableLevelIssues = this._getTableLevelIssues(table.id)

                      return (

 0 && 'warning'
                          }
                          style={{ color: this.isTableIncluded(table.id) === 'Yes' ? 'black' : 'lightGray' }}>

                            {table.name}
                            {tableLevelIssues
                              ?

                                {tableLevelIssues.length} critical table issue(s):

                                  {tableLevelIssues.map((issue, i) =>

{issue.fieldName}: {issue.description}
)}

                              : null
                            }

                          {this.isTableIncluded(table.id)}
                          {this.tableRecordCount(table.id)}
                          {issueCount}

                      )
                    })}

            </Panel>
          </Col>
        </Row>
      </Panel>
    )
  }
}
<|endoftext|>


<commit_before>import React, {Component, PropTypes} from 'react'
import { Panel, Row, Col, Table, Button, Glyphicon, Alert } from 'react-bootstrap'
import { browserHistory } from 'react-router'
import moment from 'moment'

import { getConfigProperty } from '../../common/util/config'

export default class GtfsPlusVersionSummary extends Component {
  static propTypes = {
    gtfsplus: PropTypes.object
  }
  constructor (props) {
    super(props)
    this.state = { expanded: false }
  }
  componentDidMount () {
    this.props.gtfsPlusDataRequested(this.props.version)
  }
  isTableIncluded (tableId) {
    if (!this.props.gtfsplus.tableData) return null
    return tableId in this.props.gtfsplus.tableData ? 'Yes' : 'No'
  }

  tableRecordCount (tableId) {
    if (!this.props.gtfsplus.tableData) return null
    if (!(tableId in this.props.gtfsplus.tableData)) return 'N/A'
    return this.props.gtfsplus.tableData[tableId].length.toLocaleString()
  }

  validationIssueCount (tableId) {
    if (!this.props.gtfsplus.validation) return null
    if (!(tableId in this.props.gtfsplus.validation)) return 'None'
    return this.props.gtfsplus.validation[tableId].length.toLocaleString()
  }

  feedStatus () {
    if (!this.props.gtfsplus.timestamp) return null
    if (!this.gtfsPlusEdited()) return GTFS+ data for this feed version has not been edited.
    return GTFS+ Data updated {moment(this.props.gtfsplus.timestamp).format('MMM. DD, YYYY, h:MMa')}
  }

  gtfsPlusEdited () {
    return (this.props.gtfsplus.timestamp !== this.props.version.fileTimestamp)
  }

  _toggleExpanded = () => {
    const {gtfsPlusDataRequested, version} = this.props
    const {expanded} = this.state
    if (!expanded) gtfsPlusDataRequested(version)
    this.setState({ expanded: !expanded })
  }

  render () {
    const {
      user,
      version,
      publishClicked
    } = this.props
    const { feedSource } = version
    const editingIsDisabled = !user.permissions.hasFeedPermission(feedSource.organizationId, feedSource.projectId, feedSource.id, 'edit-gtfs')
    const publishingIsDisabled = !user.permissions.hasFeedPermission(feedSource.organizationId, feedSource.projectId, feedSource.id, 'approve-gtfs')
    const header = (

 {
          if (document.activeElement === e.target && e.which === 13) {
            this._toggleExpanded()
          }
        }}
        role='button'
        tabIndex={0}
        >
        <Glyphicon glyph='check' /> GTFS+ for this Version

    )

    return (
      <Panel
        header={header}
      >
        <Row>
          <Col xs={6}>
            <Alert>{this.feedStatus()}</Alert>
          </Col>
          <Col xs={6} style={{ textAlign: 'right' }}>
            <Button
              disabled={editingIsDisabled}
              bsStyle='primary'
              onClick={() => { browserHistory.push(`/gtfsplus/${feedSource.id}/${version.id}`) }}
            >
              <Glyphicon glyph='edit' /> Edit GTFS+
            </Button>
            {this.gtfsPlusEdited()
              ? <Button
                disabled={publishingIsDisabled}
                bsStyle='primary'
                style={{ marginLeft: '6px' }}
                onClick={() => {
                  publishClicked(version)
                  this.setState({ expanded: false })
                }}
              >
                <Glyphicon glyph='upload' /> Publish as New Version
              </Button>
              : null
            }
          </Col>
        </Row>
        <Row>
          <Col xs={12}>
            <Panel>

                    Table
                    Included?
                    Records
                    Validation Issues

                  {getConfigProperty('modules.gtfsplus.spec').map((table, index) => {
                    const issueCount = this.validationIssueCount(table.id)
                    return (

 0 && 'warning'}
                        style={{ color: this.isTableIncluded(table.id) === 'Yes' ? 'black' : 'lightGray' }}>
                        {table.name}
                        {this.isTableIncluded(table.id)}
                        {this.tableRecordCount(table.id)}
                        {issueCount}

                    )
                  })}

            </Panel>
          </Col>
        </Row>
      </Panel>
    )
  }
}
<commit_msg>refactor(GtfsPlusVersionSummary): remove unused listeners<commit_after>import React, {Component, PropTypes} from 'react'
import { Panel, Row, Col, Table, Button, Glyphicon, Alert } from 'react-bootstrap'
import { browserHistory } from 'react-router'
import moment from 'moment'

import { getConfigProperty } from '../../common/util/config'

export default class GtfsPlusVersionSummary extends Component {
  static propTypes = {
    gtfsplus: PropTypes.object
  }
  constructor (props) {
    super(props)
    this.state = { expanded: false }
  }
  componentDidMount () {
    this.props.gtfsPlusDataRequested(this.props.version)
  }
  isTableIncluded (tableId) {
    if (!this.props.gtfsplus.tableData) return null
    return tableId in this.props.gtfsplus.tableData ? 'Yes' : 'No'
  }

  tableRecordCount (tableId) {
    if (!this.props.gtfsplus.tableData) return null
    if (!(tableId in this.props.gtfsplus.tableData)) return 'N/A'
    return this.props.gtfsplus.tableData[tableId].length.toLocaleString()
  }

  validationIssueCount (tableId) {
    if (!this.props.gtfsplus.validation) return null
    if (!(tableId in this.props.gtfsplus.validation)) return 'None'
    return this.props.gtfsplus.validation[tableId].length.toLocaleString()
  }

  feedStatus () {
    if (!this.props.gtfsplus.timestamp) return null
    if (!this.gtfsPlusEdited()) return GTFS+ data for this feed version has not been edited.
    return GTFS+ Data updated {moment(this.props.gtfsplus.timestamp).format('MMM. DD, YYYY, h:MMa')}
  }

  gtfsPlusEdited () {
    return (this.props.gtfsplus.timestamp !== this.props.version.fileTimestamp)
  }

  _toggleExpanded = () => {
    const {gtfsPlusDataRequested, version} = this.props
    const {expanded} = this.state
    if (!expanded) gtfsPlusDataRequested(version)
    this.setState({ expanded: !expanded })
  }

  render () {
    const {
      user,
      version,
      publishClicked
    } = this.props
    const { feedSource } = version
    const editingIsDisabled = !user.permissions.hasFeedPermission(feedSource.organizationId, feedSource.projectId, feedSource.id, 'edit-gtfs')
    const publishingIsDisabled = !user.permissions.hasFeedPermission(feedSource.organizationId, feedSource.projectId, feedSource.id, 'approve-gtfs')
    const header = (

        <Glyphicon glyph='check' /> GTFS+ for this Version

    )

    return (
      <Panel
        header={header}
      >
        <Row>
          <Col xs={6}>
            <Alert>{this.feedStatus()}</Alert>
          </Col>
          <Col xs={6} style={{ textAlign: 'right' }}>
            <Button
              disabled={editingIsDisabled}
              bsStyle='primary'
              onClick={() => { browserHistory.push(`/gtfsplus/${feedSource.id}/${version.id}`) }}
            >
              <Glyphicon glyph='edit' /> Edit GTFS+
            </Button>
            {this.gtfsPlusEdited()
              ? <Button
                disabled={publishingIsDisabled}
                bsStyle='primary'
                style={{ marginLeft: '6px' }}
                onClick={() => {
                  publishClicked(version)
                  this.setState({ expanded: false })
                }}
              >
                <Glyphicon glyph='upload' /> Publish as New Version
              </Button>
              : null
            }
          </Col>
        </Row>
        <Row>
          <Col xs={12}>
            <Panel>

                    Table
                    Included?
                    Records
                    Validation Issues

                  {getConfigProperty('modules.gtfsplus.spec').map((table, index) => {
                    const issueCount = this.validationIssueCount(table.id)
                    return (

 0 && 'warning'}
                        style={{ color: this.isTableIncluded(table.id) === 'Yes' ? 'black' : 'lightGray' }}>
                        {table.name}
                        {this.isTableIncluded(table.id)}
                        {this.tableRecordCount(table.id)}
                        {issueCount}

                    )
                  })}

            </Panel>
          </Col>
        </Row>
      </Panel>
    )
  }
}
<|endoftext|>


<commit_before>/*
Copyright (c) 2017, Dogan Yazar

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

var Tokenizer = require('./tokenizer'),
    normalizer = require('../normalizers/normalizer_sv'),
    util = require('util');

var AggressiveTokenizer = function() {
    Tokenizer.call(this);
};
util.inherits(AggressiveTokenizer, Tokenizer);

module.exports = AggressiveTokenizer;

AggressiveTokenizer.prototype.tokenize = function(text) {
    text = normalizer.remove_diacritics(text);

    // break a string up into an array of tokens by anything non-word
    return this.trim(text.split(/[^A-Za-z0-9_åÅäÄöÖ]+/));
};
<commit_msg>Add ü to the tokenizer since there are swedish words including it<commit_after>/*
Copyright (c) 2017, Dogan Yazar

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

var Tokenizer = require('./tokenizer'),
    normalizer = require('../normalizers/normalizer_sv'),
    util = require('util');

var AggressiveTokenizer = function() {
    Tokenizer.call(this);
};
util.inherits(AggressiveTokenizer, Tokenizer);

module.exports = AggressiveTokenizer;

AggressiveTokenizer.prototype.tokenize = function(text) {
    text = normalizer.remove_diacritics(text);

    // break a string up into an array of tokens by anything non-word
    // Ü is not part of swedish alphabet but there are words using it like müsli and München
    return this.trim(text.split(/[^A-Za-z0-9_åÅäÄöÖüÜ]+/));
};
<|endoftext|>


<commit_before>'use strict';

describe('auth-service', function () {

  var AuthService,
      StorageService,
      StorageServiceMock,
      loginMock = jasmine.createSpyObj('authClient', [
        '$login'
      ]);

  beforeEach(module('kosh'));

  describe('test login', function() {

    beforeEach(module(function($provide) {
      StorageServiceMock = {
        getAuthClient: function() {
          return loginMock;
        }
      };
      $provide.value('StorageService', StorageServiceMock);
    }));

    beforeEach(inject(function(_AuthService_, _StorageService_) {
      AuthService = _AuthService_;
      StorageService = _StorageService_;
    }));

    it('should call $login on the authClient object', function() {
      spyOn(StorageServiceMock, 'getAuthClient').andCallThrough();
      AuthService.login();
      expect(loginMock.$login).toHaveBeenCalledWith('github', {rememberMe: true});
    });

  });

});
<commit_msg>test logout function<commit_after>'use strict';

describe('auth-service', function () {

  var AuthService,
      StorageService,
      StorageServiceMock,
      authClientMock = jasmine.createSpyObj('authClient', [
        '$login',
        '$logout'
      ]);

  beforeEach(module('kosh'));

  describe('test login and logout', function() {

    beforeEach(module(function($provide) {
      StorageServiceMock = {
        getAuthClient: function() {
          return authClientMock;
        }
      };
      $provide.value('StorageService', StorageServiceMock);
    }));

    beforeEach(inject(function(_AuthService_, _StorageService_) {
      AuthService = _AuthService_;
      StorageService = _StorageService_;
    }));

    it('should call $login on the authClient object', function() {
      AuthService.login();
      expect(authClientMock.$login).toHaveBeenCalledWith('github', {rememberMe: true});
    });

    it('should call $logout on the authClient object', function() {
      AuthService.logout();
      expect(authClientMock.$logout).toHaveBeenCalled();
    });

  });

});
<|endoftext|>


<commit_before>'use strict';

var $ = require('jquery');
var _ = require('lodash');
var AppDispatcher = require('../dispatchers/AppDispatcher');
var Constants = require('../constants/AppConstants');

var auth2;
var subscriptions = [];
var token = {};
var channelList = [];
var loopTimes = 0;
var likesPlaylist = {
  snippet: {
    title: 'Liked from Inbox.Video',
    description: 'This playlist is generated by Inbox.Video http://inbox.video/'
  },
  status: {
    privacyStatus: 'public'
  }
};

module.exports = {
  isPlaylist: function(str) {
    return _.includes(['browse', 'browse-list', 'search', 'likes', 'picks'], str);
  },
  search: function(keyword, next) {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.LOADING,
      category: 'SEARCH'
    });

    var access_token = token.id;
    var data = {
      'part': 'snippet',
      'type': 'video',
      'q': keyword,
      'maxResults': 48,
      'access_token': access_token
    };

    if (next) {
      data.pageToken = next;
    }

    $.ajax({
      url: Constants.ActionUrls.SEARCH,
      data: data,
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      if (resp) {
        AppDispatcher.handleViewAction({
          type: Constants.ActionTypes.SEARCH,
          keyword: keyword,
          data: resp
        });
      } else {
        console.log('ajax error');
      }
    });
  },
  getVideoList: function(type, next) {
    var access_token = token.id;
    if (!access_token) {
      this.getToken(this.getVideoList);
      return false;
    }

    if (type === undefined) {
      type = 'mostPopular';
    }

    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.LOADING,
      category: type
    });

    var access_token = token.id;
    var data = {
      'part': 'snippet',
      'chart': type,
      'maxResults': 12,
      'access_token': access_token
    };

    if (next) {
      data.pageToken = next;
    }

    $.ajax({
      url: Constants.ActionUrls.VIDEO,
      data: data,
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      if (resp) {
        AppDispatcher.handleViewAction({
          type: next ? Constants.ActionTypes.TO_LIST_VIEW : Constants.ActionTypes.LOAD_CHANNEL_CENTER,
          category: type,
          data: resp
        });
      } else {
        console.log('ajax error');
      }
    });
  },
  removeChannel: function(channel) {
    var access_token = token.id;

    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.REMOVE_CHANNEL,
      id: channel.channelId
    });
    $.ajax({
      url: Constants.ActionUrls.SUBSCRIPTIONS + '?id=' + channel.subscriptionId + '&access_token=' + access_token,
      type: 'DELETE',
      xhrFields: {
        withCredentials: true
      }
    });
  },
  markAs: function(channelId, videoIds, status) {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.MARK_AS,
      channelId: channelId,
      videoIds: videoIds,
      status: status
    });
  },
  toggleEditMode: function() {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.TOGGLE_EDIT_MODE
    });
  },
  toggleSelectMode: function() {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.TOGGLE_SELECT_MODE
    });
  },
  toggleFullScreen: function() {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.TOGGLE_FULL_SCREEN
    });
  },
  openLink: function(url) {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.OPEN_LINK,
      url: url
    });
  },
  generatePlayerUrl: function(id, embed) {
    var url;
    if (!embed) {
      url = 'https://www.youtube.com/watch?v=' + id;
    } else {
      url = 'https://www.youtube.com/embed/' + id;
    }
    return url;
  },
  refresh: function(list) {
    if (!list.length) {
      return false;
    }

    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.REFRESH
    });

    var ids = _.pluck(list, 'playlistId');
    var index = 0;
    function loop() {
      setTimeout(function() {
        if (index === ids.length) {
          AppDispatcher.handleViewAction({
            type: Constants.ActionTypes.REFRESH,
            index: -1,
          });
        } else {
          var access_token = token.id;
          var data = {
            'part': 'snippet',
            'maxResults': 15,
            'playlistId': ids[index],
            'access_token': access_token
          };

          $.ajax({
            url: Constants.ActionUrls.PLAYLIST_ITEMS,
            data: data,
            xhrFields: {
              withCredentials: true
            }
          }).done(function(resp) {
            if (resp) {
              AppDispatcher.handleViewAction({
                type: Constants.ActionTypes.REFRESH,
                index: index,
                totalItemCount: resp.pageInfo.totalResults,
                data: resp.items
              });
            } else {
              console.log('ajax error');
            }
            if (index < ids.length) {
              index++;
              loop();
            }
          });
        }
      }, 10);
    }
    loop();
  },
  getVideos: function(channel, next) {
    var access_token = token.id;
    var data = {
      'part': 'snippet,contentDetails',
      'maxResults': 15,
      'playlistId': channel.playlistId,
      'access_token': access_token
    };

    if (next) {
      data.pageToken = next;
    }

    $.ajax({
      url: Constants.ActionUrls.PLAYLIST_ITEMS,
      data: data,
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      if (resp) {
        AppDispatcher.handleViewAction({
          type: Constants.ActionTypes.LOAD_VIDEOS,
          data: resp,
          channel: channel
        });
      } else {
        console.log('ajax error');
      }
    }).fail(function(resp) {
      if (resp.status === 401) {
        this.getToken();
      }
    }.bind(this));
  },
  getVideo: function(id) {
    var access_token = token.id;
    var data = {
      'part': 'snippet,player',
      'maxResults': 50,
      'id': id,
      'access_token': access_token
    };
    $.ajax({
      url: Constants.ActionUrls.VIDEO,
      data: data,
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      if (resp) {
        AppDispatcher.handleViewAction({
          type: Constants.ActionTypes.LOAD_DETAIL,
          data: resp.items[0]
        });
      }
    }).fail(function(resp) {
      if (resp.status === 401) {
        this.getToken();
      }
    }.bind(this));
  },
  getToken: function(callback) {
    var now = new Date().getTime();
    if (gapi.auth && (!token.id || token.expiresAt < now)) {
      gapi.auth.authorize(Constants.AuthObj, function(result) {
        token.id = result.access_token;
        token.expiresAt = parseInt(result.expires_at) * 1000;
        if (callback) {
          callback();
        }
      }.bind(this));
    } else {
      if (callback) {
        callback();
      }
    }
  },
  initWatched: function(channels) {
    _.each(channels, function(channel) {
      var access_token = token.id;
      var data = {
        'part': 'snippet,contentDetails',
        'maxResults': channel.newItemCount,
        'playlistId': channel.playlistId,
        'access_token': access_token
      };

      $.ajax({
        url: Constants.ActionUrls.PLAYLIST_ITEMS,
        data: data,
        xhrFields: {
          withCredentials: true
        }
      }).done(function(resp) {
        if (resp) {
          var videoIds = [];
          _.each(resp.items, function(item) {
            videoIds.push(item.snippet.resourceId.videoId);
          });

          AppDispatcher.handleViewAction({
            type: Constants.ActionTypes.MARK_AS,
            status: 'unwatched',
            channelId: channel.channelId,
            videoIds: videoIds,
            isInit: true
          });
        } else {
          console.log('ajax error');
        }
      });
    });
  },
  fetchSubscriptions: function(next) {
    var access_token = token.id;
    var data = {
      'part': 'snippet,contentDetails',
      'mine': true,
      'maxResults': 50,
      'access_token': access_token
    };

    if (next) {
      data.pageToken = next;
    }

    $.ajax({
      url: Constants.ActionUrls.SUBSCRIPTIONS,
      data: data,
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      if (resp.items.length && resp.nextPageToken) {
        subscriptions = subscriptions.concat(resp.items);
        this.fetchSubscriptions(resp.nextPageToken);
      } else if (resp) {
        subscriptions = subscriptions.concat(resp.items);
        if (subscriptions.length) {
          this.getPlaylistFromChannel(subscriptions, 0);
        }
      }
    }.bind(this));
  },
  subscribe: function(id) {
    channelList = [];
    var access_token = token.id;
    var resource = {
      snippet: {
        resourceId: {
          kind: 'youtube#channel',
          channelId: id
        }
      }
    };

    $.ajax({
      url: Constants.ActionUrls.SUBSCRIPTIONS + '?part=snippet,contentDetails&access_token=' + access_token,
      type: 'POST',
      contentType: 'application/json; charset=utf-8',
      data: JSON.stringify(resource),
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      if (resp) {
        var arr = [resp];
        this.getPlaylistFromChannel(arr, 0);
      }
    }.bind(this));
  },
  getPlaylistFromChannel: function(list, loop) {
    var channels = list.splice(0, 45);
    var ids = [];
    var access_token = token.id;
    _.each(channels, function(channel) {
      var obj = {};
      obj.subscriptionId = channel.id;
      obj.channelId = channel.snippet.resourceId.channelId;
      obj.provider = 'youtube';
      obj.newItemCount = channel.contentDetails.newItemCount;
      obj.totalItemCount = channel.contentDetails.totalItemCount;
      channelList.push(obj);
      ids.push(channel.snippet.resourceId.channelId);
    });

    var data = {
      'part': 'snippet,contentDetails',
      'id': ids.join(','),
      'maxResults': 50,
      'access_token': access_token
    };
    $.ajax({
      url: Constants.ActionUrls.CHANNEL,
      data: data,
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      _.each(resp.items, function(item, i) {
        var index = loop*45 + i;
        channelList[index]['playlistId'] = item.contentDetails.relatedPlaylists.uploads;
        channelList[index]['title'] = item.snippet.title;
        channelList[index]['description'] = item.snippet.description;
        channelList[index]['thumbnail'] = item.snippet.thumbnails.high.url;
        channelList[index]['updatedAt'] = new Date().getTime();
      }.bind(this));
      if (list.length > 0) {
        loopTimes++;
        this.getPlaylistFromChannel(list, loopTimes);
      } else {
        if (channelList.length > 1) {
          this.initWatched(channelList);
        }
        AppDispatcher.handleViewAction({
          type: Constants.ActionTypes.ADD_CHANNEL,
          channelList: channelList.reverse()
        });
      }
    }.bind(this));
  },
  initLikesPlaylist: function() {
    var access_token = token.id;
    var data = {
      'part': 'snippet,contentDetails',
      'mine': true,
      'maxResults': 50,
      'access_token': access_token
    };
    $.ajax({
      url: Constants.ActionUrls.PLAYLIST,
      data: data,
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      var likesObj;
      _.each(resp.items, function(item) {
        if(item.snippet.title === likesPlaylist.snippet.title) {
          likesObj = item;
        }
      });

      if (likesObj) {
        this.savePlaylist(likesObj, 'likes');
      } else {
        this.insertLikes();
      }
    }.bind(this));
  },
  savePlaylist: function(data, tab) {
    var obj = {};
    obj.channelId = tab;
    obj.playlistId = data.id;
    obj.provider = 'youtube';
    obj.totalItemCount = data.contentDetails.itemCount;
    obj.title = data.snippet.title;
    obj.description = data.snippet.description;
    obj.updatedAt = new Date().getTime();

    AppDispatcher.handleViewAction({
      type: tab === 'likes' ? Constants.ActionTypes.INIT_LIKES : Constants.ActionTypes.INIT_PLAYLIST,
      data: obj
    });
  },
  insertLikes: function() {
    var access_token = token.id;

    $.ajax({
      url: Constants.ActionUrls.PLAYLIST + '?part=snippet,contentDetails,status&access_token=' + access_token,
      type: 'POST',
      contentType: 'application/json; charset=utf-8',
      data: JSON.stringify(likesPlaylist),
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      if (resp) {
        this.savePlaylist(resp, 'likes');
      }
    }.bind(this));
  },
  showLikes: function(data) {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.SHOW_PAGE,
      page: 'likes'
    });

    this.getVideos(data);
  },
  showPage: function(channel, page) {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.SHOW_PAGE,
      page: page
    });

    if (channel) {
      this.getVideos(channel);
    }
  },
  toListView: function(channel, videos, selectedVideo) {
    var obj = channel;
    obj.items = videos;
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.TO_LIST_VIEW,
      data: obj,
      selectedVideo: selectedVideo
    });
    this.getVideo(selectedVideo.id);
  },
  like: function(detail, likes) {
    var access_token = token.id;
    var resource = {
      snippet: {
        playlistId: likes.playlistId,
        position: 0,
        resourceId: {
          kind: 'youtube#video',
          videoId: detail.id
        }
      }
    };
    if (!(detail.id in likes.videos)) {
      $.ajax({
        url: Constants.ActionUrls.PLAYLIST_ITEMS + '?part=snippet,contentDetails&access_token=' + access_token,
        type: 'POST',
        contentType: 'application/json; charset=utf-8',
        data: JSON.stringify(resource),
        xhrFields: {
          withCredentials: true
        }
      }).done(function(resp) {
        if (resp) {
          AppDispatcher.handleViewAction({
            type: Constants.ActionTypes.LIKE,
            id: detail.id,
            playlistItemId: resp.id
          });

        }
      }.bind(this));
    } else {
      $.ajax({
        url: Constants.ActionUrls.PLAYLIST_ITEMS + '?id=' + likes.videos[detail.id] + '&access_token=' + access_token,
        type: 'DELETE',
        xhrFields: {
          withCredentials: true
        }
      }).done(function(resp) {
        AppDispatcher.handleViewAction({
          type: Constants.ActionTypes.LIKE,
          id: detail.id
        });
      }.bind(this));
    }
  },
  createIdentity: function(profile) {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.CREATE_IDENTITY,
      email: profile.email,
      name: profile.name,
      avatar: profile.avatar,
      id: profile.id
    });

    this.getVideoList();
  }
};
<commit_msg>fix pluck<commit_after>'use strict';

var $ = require('jquery');
var _ = require('lodash');
var AppDispatcher = require('../dispatchers/AppDispatcher');
var Constants = require('../constants/AppConstants');

var auth2;
var subscriptions = [];
var token = {};
var channelList = [];
var loopTimes = 0;
var likesPlaylist = {
  snippet: {
    title: 'Liked from Inbox.Video',
    description: 'This playlist is generated by Inbox.Video http://inbox.video/'
  },
  status: {
    privacyStatus: 'public'
  }
};

module.exports = {
  isPlaylist: function(str) {
    return _.includes(['browse', 'browse-list', 'search', 'likes', 'picks'], str);
  },
  search: function(keyword, next) {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.LOADING,
      category: 'SEARCH'
    });

    var access_token = token.id;
    var data = {
      'part': 'snippet',
      'type': 'video',
      'q': keyword,
      'maxResults': 48,
      'access_token': access_token
    };

    if (next) {
      data.pageToken = next;
    }

    $.ajax({
      url: Constants.ActionUrls.SEARCH,
      data: data,
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      if (resp) {
        AppDispatcher.handleViewAction({
          type: Constants.ActionTypes.SEARCH,
          keyword: keyword,
          data: resp
        });
      } else {
        console.log('ajax error');
      }
    });
  },
  getVideoList: function(type, next) {
    var access_token = token.id;
    if (!access_token) {
      this.getToken(this.getVideoList);
      return false;
    }

    if (type === undefined) {
      type = 'mostPopular';
    }

    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.LOADING,
      category: type
    });

    var access_token = token.id;
    var data = {
      'part': 'snippet',
      'chart': type,
      'maxResults': 12,
      'access_token': access_token
    };

    if (next) {
      data.pageToken = next;
    }

    $.ajax({
      url: Constants.ActionUrls.VIDEO,
      data: data,
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      if (resp) {
        AppDispatcher.handleViewAction({
          type: next ? Constants.ActionTypes.TO_LIST_VIEW : Constants.ActionTypes.LOAD_CHANNEL_CENTER,
          category: type,
          data: resp
        });
      } else {
        console.log('ajax error');
      }
    });
  },
  removeChannel: function(channel) {
    var access_token = token.id;

    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.REMOVE_CHANNEL,
      id: channel.channelId
    });
    $.ajax({
      url: Constants.ActionUrls.SUBSCRIPTIONS + '?id=' + channel.subscriptionId + '&access_token=' + access_token,
      type: 'DELETE',
      xhrFields: {
        withCredentials: true
      }
    });
  },
  markAs: function(channelId, videoIds, status) {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.MARK_AS,
      channelId: channelId,
      videoIds: videoIds,
      status: status
    });
  },
  toggleEditMode: function() {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.TOGGLE_EDIT_MODE
    });
  },
  toggleSelectMode: function() {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.TOGGLE_SELECT_MODE
    });
  },
  toggleFullScreen: function() {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.TOGGLE_FULL_SCREEN
    });
  },
  openLink: function(url) {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.OPEN_LINK,
      url: url
    });
  },
  generatePlayerUrl: function(id, embed) {
    var url;
    if (!embed) {
      url = 'https://www.youtube.com/watch?v=' + id;
    } else {
      url = 'https://www.youtube.com/embed/' + id;
    }
    return url;
  },
  refresh: function(list) {
    if (!list.length) {
      return false;
    }

    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.REFRESH
    });

    var ids = _.map(list, 'playlistId');
    var index = 0;
    function loop() {
      setTimeout(function() {
        if (index === ids.length) {
          AppDispatcher.handleViewAction({
            type: Constants.ActionTypes.REFRESH,
            index: -1,
          });
        } else {
          var access_token = token.id;
          var data = {
            'part': 'snippet',
            'maxResults': 15,
            'playlistId': ids[index],
            'access_token': access_token
          };

          $.ajax({
            url: Constants.ActionUrls.PLAYLIST_ITEMS,
            data: data,
            xhrFields: {
              withCredentials: true
            }
          }).done(function(resp) {
            if (resp) {
              AppDispatcher.handleViewAction({
                type: Constants.ActionTypes.REFRESH,
                index: index,
                totalItemCount: resp.pageInfo.totalResults,
                data: resp.items
              });
            } else {
              console.log('ajax error');
            }
            if (index < ids.length) {
              index++;
              loop();
            }
          });
        }
      }, 10);
    }
    loop();
  },
  getVideos: function(channel, next) {
    var access_token = token.id;
    var data = {
      'part': 'snippet,contentDetails',
      'maxResults': 15,
      'playlistId': channel.playlistId,
      'access_token': access_token
    };

    if (next) {
      data.pageToken = next;
    }

    $.ajax({
      url: Constants.ActionUrls.PLAYLIST_ITEMS,
      data: data,
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      if (resp) {
        AppDispatcher.handleViewAction({
          type: Constants.ActionTypes.LOAD_VIDEOS,
          data: resp,
          channel: channel
        });
      } else {
        console.log('ajax error');
      }
    }).fail(function(resp) {
      if (resp.status === 401) {
        this.getToken();
      }
    }.bind(this));
  },
  getVideo: function(id) {
    var access_token = token.id;
    var data = {
      'part': 'snippet,player',
      'maxResults': 50,
      'id': id,
      'access_token': access_token
    };
    $.ajax({
      url: Constants.ActionUrls.VIDEO,
      data: data,
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      if (resp) {
        AppDispatcher.handleViewAction({
          type: Constants.ActionTypes.LOAD_DETAIL,
          data: resp.items[0]
        });
      }
    }).fail(function(resp) {
      if (resp.status === 401) {
        this.getToken();
      }
    }.bind(this));
  },
  getToken: function(callback) {
    var now = new Date().getTime();
    if (gapi.auth && (!token.id || token.expiresAt < now)) {
      gapi.auth.authorize(Constants.AuthObj, function(result) {
        token.id = result.access_token;
        token.expiresAt = parseInt(result.expires_at) * 1000;
        if (callback) {
          callback();
        }
      }.bind(this));
    } else {
      if (callback) {
        callback();
      }
    }
  },
  initWatched: function(channels) {
    _.each(channels, function(channel) {
      var access_token = token.id;
      var data = {
        'part': 'snippet,contentDetails',
        'maxResults': channel.newItemCount,
        'playlistId': channel.playlistId,
        'access_token': access_token
      };

      $.ajax({
        url: Constants.ActionUrls.PLAYLIST_ITEMS,
        data: data,
        xhrFields: {
          withCredentials: true
        }
      }).done(function(resp) {
        if (resp) {
          var videoIds = [];
          _.each(resp.items, function(item) {
            videoIds.push(item.snippet.resourceId.videoId);
          });

          AppDispatcher.handleViewAction({
            type: Constants.ActionTypes.MARK_AS,
            status: 'unwatched',
            channelId: channel.channelId,
            videoIds: videoIds,
            isInit: true
          });
        } else {
          console.log('ajax error');
        }
      });
    });
  },
  fetchSubscriptions: function(next) {
    var access_token = token.id;
    var data = {
      'part': 'snippet,contentDetails',
      'mine': true,
      'maxResults': 50,
      'access_token': access_token
    };

    if (next) {
      data.pageToken = next;
    }

    $.ajax({
      url: Constants.ActionUrls.SUBSCRIPTIONS,
      data: data,
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      if (resp.items.length && resp.nextPageToken) {
        subscriptions = subscriptions.concat(resp.items);
        this.fetchSubscriptions(resp.nextPageToken);
      } else if (resp) {
        subscriptions = subscriptions.concat(resp.items);
        if (subscriptions.length) {
          this.getPlaylistFromChannel(subscriptions, 0);
        }
      }
    }.bind(this));
  },
  subscribe: function(id) {
    channelList = [];
    var access_token = token.id;
    var resource = {
      snippet: {
        resourceId: {
          kind: 'youtube#channel',
          channelId: id
        }
      }
    };

    $.ajax({
      url: Constants.ActionUrls.SUBSCRIPTIONS + '?part=snippet,contentDetails&access_token=' + access_token,
      type: 'POST',
      contentType: 'application/json; charset=utf-8',
      data: JSON.stringify(resource),
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      if (resp) {
        var arr = [resp];
        this.getPlaylistFromChannel(arr, 0);
      }
    }.bind(this));
  },
  getPlaylistFromChannel: function(list, loop) {
    var channels = list.splice(0, 45);
    var ids = [];
    var access_token = token.id;
    _.each(channels, function(channel) {
      var obj = {};
      obj.subscriptionId = channel.id;
      obj.channelId = channel.snippet.resourceId.channelId;
      obj.provider = 'youtube';
      obj.newItemCount = channel.contentDetails.newItemCount;
      obj.totalItemCount = channel.contentDetails.totalItemCount;
      channelList.push(obj);
      ids.push(channel.snippet.resourceId.channelId);
    });

    var data = {
      'part': 'snippet,contentDetails',
      'id': ids.join(','),
      'maxResults': 50,
      'access_token': access_token
    };
    $.ajax({
      url: Constants.ActionUrls.CHANNEL,
      data: data,
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      _.each(resp.items, function(item, i) {
        var index = loop*45 + i;
        channelList[index]['playlistId'] = item.contentDetails.relatedPlaylists.uploads;
        channelList[index]['title'] = item.snippet.title;
        channelList[index]['description'] = item.snippet.description;
        channelList[index]['thumbnail'] = item.snippet.thumbnails.high.url;
        channelList[index]['updatedAt'] = new Date().getTime();
      }.bind(this));
      if (list.length > 0) {
        loopTimes++;
        this.getPlaylistFromChannel(list, loopTimes);
      } else {
        if (channelList.length > 1) {
          this.initWatched(channelList);
        }
        AppDispatcher.handleViewAction({
          type: Constants.ActionTypes.ADD_CHANNEL,
          channelList: channelList.reverse()
        });
      }
    }.bind(this));
  },
  initLikesPlaylist: function() {
    var access_token = token.id;
    var data = {
      'part': 'snippet,contentDetails',
      'mine': true,
      'maxResults': 50,
      'access_token': access_token
    };
    $.ajax({
      url: Constants.ActionUrls.PLAYLIST,
      data: data,
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      var likesObj;
      _.each(resp.items, function(item) {
        if(item.snippet.title === likesPlaylist.snippet.title) {
          likesObj = item;
        }
      });

      if (likesObj) {
        this.savePlaylist(likesObj, 'likes');
      } else {
        this.insertLikes();
      }
    }.bind(this));
  },
  savePlaylist: function(data, tab) {
    var obj = {};
    obj.channelId = tab;
    obj.playlistId = data.id;
    obj.provider = 'youtube';
    obj.totalItemCount = data.contentDetails.itemCount;
    obj.title = data.snippet.title;
    obj.description = data.snippet.description;
    obj.updatedAt = new Date().getTime();

    AppDispatcher.handleViewAction({
      type: tab === 'likes' ? Constants.ActionTypes.INIT_LIKES : Constants.ActionTypes.INIT_PLAYLIST,
      data: obj
    });
  },
  insertLikes: function() {
    var access_token = token.id;

    $.ajax({
      url: Constants.ActionUrls.PLAYLIST + '?part=snippet,contentDetails,status&access_token=' + access_token,
      type: 'POST',
      contentType: 'application/json; charset=utf-8',
      data: JSON.stringify(likesPlaylist),
      xhrFields: {
        withCredentials: true
      }
    }).done(function(resp) {
      if (resp) {
        this.savePlaylist(resp, 'likes');
      }
    }.bind(this));
  },
  showLikes: function(data) {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.SHOW_PAGE,
      page: 'likes'
    });

    this.getVideos(data);
  },
  showPage: function(channel, page) {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.SHOW_PAGE,
      page: page
    });

    if (channel) {
      this.getVideos(channel);
    }
  },
  toListView: function(channel, videos, selectedVideo) {
    var obj = channel;
    obj.items = videos;
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.TO_LIST_VIEW,
      data: obj,
      selectedVideo: selectedVideo
    });
    this.getVideo(selectedVideo.id);
  },
  like: function(detail, likes) {
    var access_token = token.id;
    var resource = {
      snippet: {
        playlistId: likes.playlistId,
        position: 0,
        resourceId: {
          kind: 'youtube#video',
          videoId: detail.id
        }
      }
    };
    if (!(detail.id in likes.videos)) {
      $.ajax({
        url: Constants.ActionUrls.PLAYLIST_ITEMS + '?part=snippet,contentDetails&access_token=' + access_token,
        type: 'POST',
        contentType: 'application/json; charset=utf-8',
        data: JSON.stringify(resource),
        xhrFields: {
          withCredentials: true
        }
      }).done(function(resp) {
        if (resp) {
          AppDispatcher.handleViewAction({
            type: Constants.ActionTypes.LIKE,
            id: detail.id,
            playlistItemId: resp.id
          });

        }
      }.bind(this));
    } else {
      $.ajax({
        url: Constants.ActionUrls.PLAYLIST_ITEMS + '?id=' + likes.videos[detail.id] + '&access_token=' + access_token,
        type: 'DELETE',
        xhrFields: {
          withCredentials: true
        }
      }).done(function(resp) {
        AppDispatcher.handleViewAction({
          type: Constants.ActionTypes.LIKE,
          id: detail.id
        });
      }.bind(this));
    }
  },
  createIdentity: function(profile) {
    AppDispatcher.handleViewAction({
      type: Constants.ActionTypes.CREATE_IDENTITY,
      email: profile.email,
      name: profile.name,
      avatar: profile.avatar,
      id: profile.id
    });

    this.getVideoList();
  }
};
<|endoftext|>


<commit_before>(function(W, Hooray) {
    "use strict";

    Hooray.defineClass('Billiard', '', 'GameRenderEngine', {
        init: function(gameContainerId) {
            Hooray.log('A new Billiard.GameRenderEngine instance has been created within "'+gameContainerId+'"!');

            // init gameContainer
            this.gameContainer = this.initGameContainer(gameContainerId);

            // init asset loader
            this.assetLoader = new Billiard.AssetLoader();

            // init renderer
            this.renderer = this.initRenderer(this.gameContainer);

            // init scene
            this.scene = new THREE.Scene();

            // create a camera, position it and add it to the scene
            this.camera = this.initCamera(this.gameContainer, this.scene);

            // init light
            this.light = this.initLight(this.scene);
        },

        // TODO: refactor this function
        initGameRenderEngine: function(table, balls, cues, pockets) {
            var that = this;
            var ballId, cueId, pocketId, geometry, material, sphere, plane, pocket;
            var rh = Billiard.Helper.RotationHelper;

            var promises = [];

            // TODO: load table texture
            geometry = new THREE.PlaneGeometry(table.getPlayingFieldWidth(), table.getPlayingFieldHeight());
            material = new THREE.MeshBasicMaterial({color: 0x00ff00});
            plane = new THREE.Mesh(geometry, material);
            plane.position.z = -1;
            that.scene.add(plane);

            // load ball textures
            var ballTexturesPromise = this.assetLoader.getMaps(Object.keys(balls)).then(function(mapHash) {
                // create three.js sphere that represents a ball and add it to the scene
                for (ballId in balls) {
                    if (balls.hasOwnProperty(ballId)) {
                        geometry = new THREE.SphereGeometry(balls[ballId].radius, 24, 24);
                        material = new THREE.MeshPhongMaterial({
                            map: mapHash[ballId]
                            //shininess: 52
                            //color: 0x00FF00
                        });
                        sphere = new THREE.Mesh(geometry, material);
                        sphere.position.x = balls[ballId].initX;
                        sphere.position.y = balls[ballId].initY;

                        // adjust rotation because the textures has got a particular offset
                        rh.rotateAroundWorldAxisY(sphere, -Math.PI/2);

                        that.scene.add(sphere);

                        // augment the ball with the created mesh
                        balls[ballId].mesh = sphere;

                        // TODO: this must be solved in a better way
                        // augment cues with ball0 property
                        if (ballId === 'images/ball0.jpg') {
                            for (cueId in cues) {
                                if (cues.hasOwnProperty(cueId)) {
                                    cues[cueId].augment('ball0', balls[ballId]);
                                }
                            }
                        }
                    }
                }
            });
            promises.push(ballTexturesPromise);

            // TODO: load cue textures in the future
            for (cueId in cues) {
                if (cues.hasOwnProperty(cueId)) {
                    geometry = new THREE.PlaneGeometry( 6, 480 );
                    material = new THREE.MeshBasicMaterial( {color: 0xffff00} );
                    plane = new THREE.Mesh( geometry, material );

                    plane.position.z = 20;
                    //plane.rotation.z = Math.PI / 4;

                    that.scene.add( plane );

                    // augment the cue with the created mesh
                    cues[cueId].augment('mesh', plane);

                    // augment the cue with the gameContainer DOM element
                    cues[cueId].augment('gameContainer', that.gameContainer);
                }
            }

            // TODO: load pocket textures in the future
            for (pocketId in pockets) {
                if (pockets.hasOwnProperty(pocketId)) {
                    material = new THREE.MeshBasicMaterial({color: 0xff4306});
                    geometry = new THREE.CircleGeometry(pockets[pocketId].radius, 32);
                    pocket = new THREE.Mesh(geometry, material);
                    pocket.position.x = pockets[pocketId].x;
                    pocket.position.y = pockets[pocketId].y;
                    that.scene.add(pocket);

                    // augment the pocket with the created mesh
                    pockets[pocketId].augment('mesh', pocket);
                }
            }

            return Q.all(promises).then(function() {
                return function() {
                    that.render();
                };
            });
        },

        render: function() {
            this.renderer.render(this.scene, this.camera);
        },

        initGameContainer: function(gameContainerId) {
            var gameContainer = W.document.getElementById(gameContainerId);
            return {
                domElement: gameContainer,
                width: gameContainer.offsetWidth,
                height: gameContainer.offsetHeight
            };
        },

        initRenderer: function(gameContainer) {
            var renderer = Detector.webgl ? new THREE.WebGLRenderer() : new THREE.CanvasRenderer();
            renderer.setSize(gameContainer.width, gameContainer.height);
            gameContainer.domElement.appendChild(renderer.domElement);

            return renderer;
        },

        initCamera: function(gameContainer, scene) {
            var width   = gameContainer.width,
                height  = gameContainer.height,
                left    = width / -2,
                right   = width / 2,
                top     = height / 2,
                bottom  = height / -2,
                near    = 1,
                far     = 1000,
                camera  = new THREE.OrthographicCamera(
                    left, right,
                    top, bottom,
                    near, far
                );

            camera.position.z = 400;
            scene.add(camera);

            Hooray.log(
                'Initializing camera! [left, right, top, bottom, near, far]',
                left, right, top, bottom, near, far
            );

            return camera;
        },

        initLight: function(scene) {
            var light = new THREE.DirectionalLight(0xFFFFFF, 1);

            light.position.set(0, 0, 1);
            scene.add(light);

            return light;
        }
    });

})(window, Hooray);
<commit_msg>change table color<commit_after>(function(W, Hooray) {
    "use strict";

    Hooray.defineClass('Billiard', '', 'GameRenderEngine', {
        init: function(gameContainerId) {
            Hooray.log('A new Billiard.GameRenderEngine instance has been created within "'+gameContainerId+'"!');

            // init gameContainer
            this.gameContainer = this.initGameContainer(gameContainerId);

            // init asset loader
            this.assetLoader = new Billiard.AssetLoader();

            // init renderer
            this.renderer = this.initRenderer(this.gameContainer);

            // init scene
            this.scene = new THREE.Scene();

            // create a camera, position it and add it to the scene
            this.camera = this.initCamera(this.gameContainer, this.scene);

            // init light
            this.light = this.initLight(this.scene);
        },

        // TODO: refactor this function
        initGameRenderEngine: function(table, balls, cues, pockets) {
            var that = this;
            var ballId, cueId, pocketId, geometry, material, sphere, plane, pocket;
            var rh = Billiard.Helper.RotationHelper;

            var promises = [];

            // TODO: load table texture
            geometry = new THREE.PlaneGeometry(table.getPlayingFieldWidth(), table.getPlayingFieldHeight());
            material = new THREE.MeshBasicMaterial({color: 0x008000});
            plane = new THREE.Mesh(geometry, material);
            plane.position.z = -1;
            that.scene.add(plane);

            // load ball textures
            var ballTexturesPromise = this.assetLoader.getMaps(Object.keys(balls)).then(function(mapHash) {
                // create three.js sphere that represents a ball and add it to the scene
                for (ballId in balls) {
                    if (balls.hasOwnProperty(ballId)) {
                        geometry = new THREE.SphereGeometry(balls[ballId].radius, 24, 24);
                        material = new THREE.MeshPhongMaterial({
                            map: mapHash[ballId]
                            //shininess: 52
                            //color: 0x00FF00
                        });
                        sphere = new THREE.Mesh(geometry, material);
                        sphere.position.x = balls[ballId].initX;
                        sphere.position.y = balls[ballId].initY;

                        // adjust rotation because the textures has got a particular offset
                        rh.rotateAroundWorldAxisY(sphere, -Math.PI/2);

                        that.scene.add(sphere);

                        // augment the ball with the created mesh
                        balls[ballId].mesh = sphere;

                        // TODO: this must be solved in a better way
                        // augment cues with ball0 property
                        if (ballId === 'images/ball0.jpg') {
                            for (cueId in cues) {
                                if (cues.hasOwnProperty(cueId)) {
                                    cues[cueId].augment('ball0', balls[ballId]);
                                }
                            }
                        }
                    }
                }
            });
            promises.push(ballTexturesPromise);

            // TODO: load cue textures in the future
            for (cueId in cues) {
                if (cues.hasOwnProperty(cueId)) {
                    geometry = new THREE.PlaneGeometry( 6, 480 );
                    material = new THREE.MeshBasicMaterial( {color: 0xffff00} );
                    plane = new THREE.Mesh( geometry, material );

                    plane.position.z = 20;
                    //plane.rotation.z = Math.PI / 4;

                    that.scene.add( plane );

                    // augment the cue with the created mesh
                    cues[cueId].augment('mesh', plane);

                    // augment the cue with the gameContainer DOM element
                    cues[cueId].augment('gameContainer', that.gameContainer);
                }
            }

            // TODO: load pocket textures in the future
            for (pocketId in pockets) {
                if (pockets.hasOwnProperty(pocketId)) {
                    material = new THREE.MeshBasicMaterial({color: 0xff4306});
                    geometry = new THREE.CircleGeometry(pockets[pocketId].radius, 32);
                    pocket = new THREE.Mesh(geometry, material);
                    pocket.position.x = pockets[pocketId].x;
                    pocket.position.y = pockets[pocketId].y;
                    that.scene.add(pocket);

                    // augment the pocket with the created mesh
                    pockets[pocketId].augment('mesh', pocket);
                }
            }

            return Q.all(promises).then(function() {
                return function() {
                    that.render();
                };
            });
        },

        render: function() {
            this.renderer.render(this.scene, this.camera);
        },

        initGameContainer: function(gameContainerId) {
            var gameContainer = W.document.getElementById(gameContainerId);
            return {
                domElement: gameContainer,
                width: gameContainer.offsetWidth,
                height: gameContainer.offsetHeight
            };
        },

        initRenderer: function(gameContainer) {
            var renderer = Detector.webgl ? new THREE.WebGLRenderer() : new THREE.CanvasRenderer();
            renderer.setSize(gameContainer.width, gameContainer.height);
            gameContainer.domElement.appendChild(renderer.domElement);

            return renderer;
        },

        initCamera: function(gameContainer, scene) {
            var width   = gameContainer.width,
                height  = gameContainer.height,
                left    = width / -2,
                right   = width / 2,
                top     = height / 2,
                bottom  = height / -2,
                near    = 1,
                far     = 1000,
                camera  = new THREE.OrthographicCamera(
                    left, right,
                    top, bottom,
                    near, far
                );

            camera.position.z = 400;
            scene.add(camera);

            Hooray.log(
                'Initializing camera! [left, right, top, bottom, near, far]',
                left, right, top, bottom, near, far
            );

            return camera;
        },

        initLight: function(scene) {
            var light = new THREE.DirectionalLight(0xFFFFFF, 1);

            light.position.set(0, 0, 1);
            scene.add(light);

            return light;
        }
    });

})(window, Hooray);
<|endoftext|>


<commit_before>// -------------------------------------------------------------------------- \\
// File: RecordAttribute.js                                                   \\
// Module: DataStore                                                          \\
// Requires: Core, Foundation, Record.js                                      \\
// Author: Neil Jenkins                                                       \\
// License: © 2010-2015 FastMail Pty Ltd. MIT Licensed.                       \\
// -------------------------------------------------------------------------- \\

"use strict";

( function ( NS, undefined ) {

var instanceOf = function ( value, Type ) {
    switch ( typeof value ) {
        case 'string':
            return Type === String;
        case 'boolean':
            return Type === Boolean;
        case 'number':
            return Type === Number;
    }
    return value instanceof Type;
};

/**
    Class: O.RecordAttribute

    Represents an attribute on a record.
*/
var RecordAttribute = NS.Class({

    __setupProperty__: function ( metadata, propKey, object ) {
        var attrs = metadata.attrs,
            dependents;
        if ( !metadata.hasOwnProperty( 'attrs' ) ) {
            attrs = metadata.attrs = attrs ? Object.create( attrs ) : {};
        }
        if ( this.isPrimaryKey ) {
            object.constructor.primaryKey = propKey;
            // Make the `id` property depend on the primary key.
            dependents = metadata.dependents;
            if ( !metadata.hasOwnProperty( 'dependents' ) ) {
                dependents = metadata.dependents = NS.clone( dependents );
                metadata.allDependents = {};
            }
            ( dependents[ propKey ] ||
                ( dependents[ propKey ] = [] ) ).push( 'id' );
        }
        attrs[ this.key || propKey ] = propKey;
        object.constructor.clientSettableAttributes = null;
    },

    __teardownProperty__: function ( metadata, propKey, object ) {
        var attrs = metadata.attrs;
        if ( !metadata.hasOwnProperty( 'attrs' ) ) {
            attrs = metadata.attrs = Object.create( attrs );
        }
        attrs[ this.key || propKey ] = null;
        object.constructor.clientSettableAttributes = null;
    },

    /**
        Constructor: O.RecordAttribute

        Parameters:
            mixin - {Object} (optional) Override the default properties.
    */
    init: function ( mixin ) {
        NS.extend( this, mixin );
    },

    /**
        Property (private): O.RecordAttribute#isProperty
        Type: Boolean
        Default: true

        Record attributes are computed properties.
    */
    isProperty: true,
    /**
        Property (private): O.RecordAttribute#isVolatile
        Type: Boolean
        Default: false

        Record attributes should be cached.
    */
    isVolatile: false,
    /**
        Property (private): O.RecordAttribute#isSilent
        Type: Boolean
        Default: true

        Store will handle firing computedPropertyIsChanged on record.
    */
    isSilent: true,

    /**
        Property: O.RecordAttribute#noSync
        Type: Boolean
        Default: false

        If set to true, changes will not be propagated back to the source.
    */
    noSync: false,

    /**
        Property: O.RecordAttribute#Type
        Type: Constructor
        Default: null

        If a type is set and it has a fromJSON method, this will be used to
        convert values from the underlying data object when the attribute is
        fetched.
    */
    Type: null,

    /**
        Property: O.RecordAttribute#isNullable
        Type: Boolean
        Default: true

        If false, attempts to set null for the value will throw an error.
    */
    isNullable: true,

    /**
        Property: O.RecordAttribute#key
        Type: {String|null}
        Default: null

        The key to use on the JSON object for this attribute. If not set, will
        use the same key as the property name on the record.
    */
    key: null,

    /**
        Property: O.RecordAttribute#isPrimaryKey
        Type: Boolean
        Default: true

        If true, this is the primary key for the record.
    */
    isPrimaryKey: false,

    /**
        Method: O.RecordAttribute#willSet

        This function is used to check the value being set is permissible. By
        default, it checks that the value is not null (or the <#isNullable>
        property is true), and that the value is of the correct type (if the
        <#Type> property is set). An error is thrown if the value is of a
        different type.

        You could override this function to, for example, only allow values that
        pass a strict validation to be set.

        Parameters:
            propValue - {*} The value being set.
            propKey   - {String} The name of the attribute.

        Returns:
            {Boolean} May the value be set?
    */
    willSet: function ( propValue, propKey, record ) {
        if ( !record.get( 'isEditable' ) ) {
            return false;
        }
        if ( propValue === null ) {
            if ( !this.isNullable ) {
                return false;
            }
        }
        else if ( this.Type && !instanceOf( propValue, this.Type ) ) {
            throw new Error(
                'Incorrect value type for record attribute: \n' +
                'key: ' + propKey + '\n' +
                'value: ' + propValue
            );
        }
        return true;
    },

    /**
        Property: O.RecordAttribute#defaultValue
        Type: *
        Default: undefined

        If the attribute is not set on the underlying data object, the
        defaultValue will be returned instead. This will also be used to add
        this attribute to the data object if a new record is created and the
        attribute is not set.

        The value should be of the type specified in <O.RecordAttribute#Type>.
    */
    defaultValue: undefined,

    /**
        Method: O.RecordAttribute#validate

        Tests whether the value to be set is valid.

        Parameters:
            propValue   - {*} The value being set. This is the real value, not
                          the serialised version for JSON (if different).
            propKey     - {String} The name of the attribute on the record.
            record      - {O.Record} The record on which the value is being set.

        Returns:
            {O.ValidationError} An object describing the error if this is not a
            valid value for the attribute. Otherwise, returns null if the value
            is valid.
    */
    validate: null,

    /**
        Property: O.RecordAttribute#validityDependencies
        Type: String[]|null
        Default: null

        Other attributes the validity depends on. The attribute will be
        revalidated if any of these attributes change. Note, chained
        dependencies are not automatically calculated; you must explicitly state
        all dependencies.

        NB. This is a list of the names of the attributes as used on the
        objects, not necessarily that of the underlying keys used in the JSON
        data object.
    */
    validityDependencies: null,

    /**
        Method: O.RecordAttribute#call

        Gets/sets the attribute.

        Parameters:
            record    - {O.Record} The record the attribute is being set on or
                        got from.
            propValue - {*} The value being set (undefined if just a 'get').
            propKey   - {String} The name of the attribute on the record.

        Returns:
            {*} The attribute.
    */
    call: function ( record, propValue, propKey ) {
        var store = record.get( 'store' ),
            storeKey = record.get( 'storeKey' ),
            data = storeKey ? store.getData( storeKey ) : record._data,
            attrKey, attrValue, currentAttrValue, update, Type;
        if ( data ) {
            attrKey = this.key || propKey;
            currentAttrValue = data[ attrKey ];
            if ( propValue !== undefined &&
                    this.willSet( propValue, propKey, record ) ) {
                attrValue = propValue && propValue.toJSON ?
                    propValue.toJSON() : propValue;
                if ( !NS.isEqual( attrValue, currentAttrValue ) ) {
                    if ( storeKey ) {
                        update = {};
                        update[ attrKey ] = attrValue;
                        store.updateData( storeKey, update,
                            !( this.noSync || record._noSync ) );
                        store.fire( 'record:user:update', { record: this } );
                    } else {
                        data[ attrKey ] = attrValue;
                        record.computedPropertyDidChange( propKey );
                        if ( this.validate ) {
                            record.get( 'errorForAttribute' ).set( propKey,
                                this.validate( propValue, propKey, record ) );
                        }
                    }
                }
                return propValue;
            }
            Type = this.Type;
        }
        return currentAttrValue !== undefined ?
            currentAttrValue !== null && Type && Type.fromJSON ?
                Type.fromJSON( currentAttrValue ) : currentAttrValue :
            this.defaultValue;
    }
});

NS.RecordAttribute = RecordAttribute;

/**
    Function: O.Record.attr

    A factory function for creating a new <O.RecordAttribute> instance. This
    will set an assert function to verify the correct type is being set whenever
    the value is set, and that the correct type is used to serialise to/from
    primitive types.

    When subclassing O.Record, use this function to create a value for any
    properties on the record which correspond to properties on the underlying
    data object. This will automatically set things up so they are fetched from
    the store and synced to the source.

    Parameters:
        Type    - {Constructor} The type of the property.
        mixin - {Object} Properties to pass to the <O.RecordAttribute>
                constructor.

    Returns:
        {O.RecordAttribute} Getter/setter for that record attribute.
*/
NS.Record.attr = function ( Type, mixin ) {
    if ( !mixin ) { mixin = {}; }
    if ( Type && !mixin.Type ) { mixin.Type = Type; }
    return new RecordAttribute( mixin );
};

}( O ) );
<commit_msg>Add O.RecordAttribute#toJSON property<commit_after>// -------------------------------------------------------------------------- \\
// File: RecordAttribute.js                                                   \\
// Module: DataStore                                                          \\
// Requires: Core, Foundation, Record.js                                      \\
// Author: Neil Jenkins                                                       \\
// License: © 2010-2015 FastMail Pty Ltd. MIT Licensed.                       \\
// -------------------------------------------------------------------------- \\

"use strict";

( function ( NS, undefined ) {

var instanceOf = function ( value, Type ) {
    switch ( typeof value ) {
        case 'string':
            return Type === String;
        case 'boolean':
            return Type === Boolean;
        case 'number':
            return Type === Number;
    }
    return value instanceof Type;
};

/**
    Class: O.RecordAttribute

    Represents an attribute on a record.
*/
var RecordAttribute = NS.Class({

    __setupProperty__: function ( metadata, propKey, object ) {
        var attrs = metadata.attrs,
            dependents;
        if ( !metadata.hasOwnProperty( 'attrs' ) ) {
            attrs = metadata.attrs = attrs ? Object.create( attrs ) : {};
        }
        if ( this.isPrimaryKey ) {
            object.constructor.primaryKey = propKey;
            // Make the `id` property depend on the primary key.
            dependents = metadata.dependents;
            if ( !metadata.hasOwnProperty( 'dependents' ) ) {
                dependents = metadata.dependents = NS.clone( dependents );
                metadata.allDependents = {};
            }
            ( dependents[ propKey ] ||
                ( dependents[ propKey ] = [] ) ).push( 'id' );
        }
        attrs[ this.key || propKey ] = propKey;
        object.constructor.clientSettableAttributes = null;
    },

    __teardownProperty__: function ( metadata, propKey, object ) {
        var attrs = metadata.attrs;
        if ( !metadata.hasOwnProperty( 'attrs' ) ) {
            attrs = metadata.attrs = Object.create( attrs );
        }
        attrs[ this.key || propKey ] = null;
        object.constructor.clientSettableAttributes = null;
    },

    /**
        Constructor: O.RecordAttribute

        Parameters:
            mixin - {Object} (optional) Override the default properties.
    */
    init: function ( mixin ) {
        NS.extend( this, mixin );
    },

    /**
        Property (private): O.RecordAttribute#isProperty
        Type: Boolean
        Default: true

        Record attributes are computed properties.
    */
    isProperty: true,
    /**
        Property (private): O.RecordAttribute#isVolatile
        Type: Boolean
        Default: false

        Record attributes should be cached.
    */
    isVolatile: false,
    /**
        Property (private): O.RecordAttribute#isSilent
        Type: Boolean
        Default: true

        Store will handle firing computedPropertyIsChanged on record.
    */
    isSilent: true,

    /**
        Property: O.RecordAttribute#noSync
        Type: Boolean
        Default: false

        If set to true, changes will not be propagated back to the source.
    */
    noSync: false,

    /**
        Property: O.RecordAttribute#Type
        Type: Constructor
        Default: null

        If a type is set and it has a fromJSON method, this will be used to
        convert values from the underlying data object when the attribute is
        fetched.
    */
    Type: null,

    /**
        Property: O.RecordAttribute#isNullable
        Type: Boolean
        Default: true

        If false, attempts to set null for the value will throw an error.
    */
    isNullable: true,

    /**
        Property: O.RecordAttribute#key
        Type: {String|null}
        Default: null

        The key to use on the JSON object for this attribute. If not set, will
        use the same key as the property name on the record.
    */
    key: null,

    /**
        Property: O.RecordAttribute#isPrimaryKey
        Type: Boolean
        Default: true

        If true, this is the primary key for the record.
    */
    isPrimaryKey: false,

    /**
        Method: O.RecordAttribute#willSet

        This function is used to check the value being set is permissible. By
        default, it checks that the value is not null (or the <#isNullable>
        property is true), and that the value is of the correct type (if the
        <#Type> property is set). An error is thrown if the value is of a
        different type.

        You could override this function to, for example, only allow values that
        pass a strict validation to be set.

        Parameters:
            propValue - {*} The value being set.
            propKey   - {String} The name of the attribute.

        Returns:
            {Boolean} May the value be set?
    */
    willSet: function ( propValue, propKey, record ) {
        if ( !record.get( 'isEditable' ) ) {
            return false;
        }
        if ( propValue === null ) {
            if ( !this.isNullable ) {
                return false;
            }
        }
        else if ( this.Type && !instanceOf( propValue, this.Type ) ) {
            throw new Error(
                'Incorrect value type for record attribute: \n' +
                'key: ' + propKey + '\n' +
                'value: ' + propValue
            );
        }
        return true;
    },

    /**
        Property: O.RecordAttribute#toJSON
        Type: *
        Default: null|(*,String,O.Record)->*

        If set, this function will be used to convert the property to a
        JSON-compatible representation. The function will be called as a method
        on the RecordAttribute object, and passed the following arguments:

        propValue - {*} The value to convert.
        propKey   - {String} The name of the attribute.
        record    - {O.Record} The record the attribute is being set on or
                    got from.
    */
    toJSON: null,

    /**
        Property: O.RecordAttribute#defaultValue
        Type: *
        Default: undefined

        If the attribute is not set on the underlying data object, the
        defaultValue will be returned instead. This will also be used to add
        this attribute to the data object if a new record is created and the
        attribute is not set.

        The value should be of the type specified in <O.RecordAttribute#Type>.
    */
    defaultValue: undefined,

    /**
        Method: O.RecordAttribute#validate

        Tests whether the value to be set is valid.

        Parameters:
            propValue   - {*} The value being set. This is the real value, not
                          the serialised version for JSON (if different).
            propKey     - {String} The name of the attribute on the record.
            record      - {O.Record} The record on which the value is being set.

        Returns:
            {O.ValidationError} An object describing the error if this is not a
            valid value for the attribute. Otherwise, returns null if the value
            is valid.
    */
    validate: null,

    /**
        Property: O.RecordAttribute#validityDependencies
        Type: String[]|null
        Default: null

        Other attributes the validity depends on. The attribute will be
        revalidated if any of these attributes change. Note, chained
        dependencies are not automatically calculated; you must explicitly state
        all dependencies.

        NB. This is a list of the names of the attributes as used on the
        objects, not necessarily that of the underlying keys used in the JSON
        data object.
    */
    validityDependencies: null,

    /**
        Method: O.RecordAttribute#call

        Gets/sets the attribute.

        Parameters:
            record    - {O.Record} The record the attribute is being set on or
                        got from.
            propValue - {*} The value being set (undefined if just a 'get').
            propKey   - {String} The name of the attribute on the record.

        Returns:
            {*} The attribute.
    */
    call: function ( record, propValue, propKey ) {
        var store = record.get( 'store' );
        var storeKey = record.get( 'storeKey' );
        var data = storeKey ? store.getData( storeKey ) : record._data;
        var attrKey, attrValue, currentAttrValue, update, Type;
        if ( data ) {
            attrKey = this.key || propKey;
            currentAttrValue = data[ attrKey ];
            if ( propValue !== undefined &&
                    this.willSet( propValue, propKey, record ) ) {
                if ( this.toJSON ) {
                    attrValue = this.toJSON( propValue, propKey, record );
                } else if ( propValue && propValue.toJSON ) {
                    attrValue = propValue.toJSON();
                } else {
                    attrValue = propValue;
                }
                if ( !NS.isEqual( attrValue, currentAttrValue ) ) {
                    if ( storeKey ) {
                        update = {};
                        update[ attrKey ] = attrValue;
                        store.updateData( storeKey, update,
                            !( this.noSync || record._noSync ) );
                        store.fire( 'record:user:update', { record: this } );
                    } else {
                        data[ attrKey ] = attrValue;
                        record.computedPropertyDidChange( propKey );
                        if ( this.validate ) {
                            record.get( 'errorForAttribute' ).set( propKey,
                                this.validate( propValue, propKey, record ) );
                        }
                    }
                }
                return propValue;
            }
            Type = this.Type;
        }
        return currentAttrValue !== undefined ?
            currentAttrValue !== null && Type && Type.fromJSON ?
                Type.fromJSON( currentAttrValue ) : currentAttrValue :
            this.defaultValue;
    }
});

NS.RecordAttribute = RecordAttribute;

/**
    Function: O.Record.attr

    A factory function for creating a new <O.RecordAttribute> instance. This
    will set an assert function to verify the correct type is being set whenever
    the value is set, and that the correct type is used to serialise to/from
    primitive types.

    When subclassing O.Record, use this function to create a value for any
    properties on the record which correspond to properties on the underlying
    data object. This will automatically set things up so they are fetched from
    the store and synced to the source.

    Parameters:
        Type    - {Constructor} The type of the property.
        mixin - {Object} Properties to pass to the <O.RecordAttribute>
                constructor.

    Returns:
        {O.RecordAttribute} Getter/setter for that record attribute.
*/
NS.Record.attr = function ( Type, mixin ) {
    if ( !mixin ) { mixin = {}; }
    if ( Type && !mixin.Type ) { mixin.Type = Type; }
    return new RecordAttribute( mixin );
};

}( O ) );
<|endoftext|>