<commit_before>/*
 * moan
 * https://github.com/neocotic/moan
 *
 * Copyright (c) 2015 Alasdair Mercer
 * Licensed under the MIT license.
 * https://github.com/neocotic/moan/blob/master/LICENSE.md
 */

'use strict'

const EventEmitter = require('events').EventEmitter
const expect = require('expect.js')
const fs = require('fs')
const sinon = require('sinon')

const singleton = require('../lib/moan')
const Moan = singleton.Moan

describe('moan', () => {
  it('should be a singleton instance of Moan', () => {
    expect(singleton).to.be.ok()
    expect(singleton).to.be.a(Moan)
  })
})

describe('Moan', () => {
  let moan

  beforeEach(() => {
    moan = new Moan()
    moan.log = sinon.stub(moan.log)
  })

  it('should extend from EventEmitter', () => {
    expect(moan).to.be.an(EventEmitter)
  })

  describe('#constructor', () => {
    // TODO: Complete unit tests
  })

  describe('#config', () => {
    context('when only configuration key is provided', () => {
      it('should return configuration value', () => {
        moan.config('foo', 'bar')

        expect(moan.config('foo')).to.be('bar')
      })

      it('should return nothing if key does not exist', () => {
        expect(moan.config('foo')).not.to.be.ok()
      })
    })

    context('when configuration key and value are provided', () => {
      it('should return newly configured value', () => {
        expect(moan.config('foo', 'bar')).to.be('bar')
      })

      it('should replace any previously configured value', () => {
        expect(moan.config('foo', 'bar')).to.be('bar')
        expect(moan.config('foo', 'baz')).to.be('baz')

        expect(moan.config('foo')).to.be('baz')
      })
    })
  })

  describe('#configs', () => {
    it('should be contain all unique configurations', () => {
      moan.config('foo', 'bar')
      moan.config('fu', 'baz')
      moan.config('foo', 'buzz')

      expect(moan.configs).to.eql([ 'foo', 'fu' ])
    })

    context('when nothing has been configured', () => {
      it('should be an empty array', () => {
        expect(moan.configs).to.eql([])
      })
    })

    it('should be read-only', () => {
      try {
        moan.configs = [ 'foo' ]

        expect().fail('Should have thrown error')
      } catch (error) {
        expect(error).to.be.a(TypeError)
      }
    })

    it('should not allow modifications', () => {
      moan.configs.push('foo')

      expect(moan.configs).to.eql([])
    })
  })

  describe('#currentTask', () => {
    context('when idle', () => {
      it('should be nothing', () => {
        expect(moan.currentTask).not.to.be.ok()
      })
    })

    context('when tasks have finished successfully', () => {
      it('should be nothing', (done) => {
        moan.task('good', () => Promise.resolve('Yipee!'))
        moan.task('default', 'good')

        moan.run()
          .then(() => {
            expect(moan.currentTask).not.to.be.ok()
          })
          .then(done, done)
      })
    })

    context('when tasks have finished erroneously', () => {
      it('should be task that failed for debugging purposes', (done) => {
        moan.task('bad', () => Promise.reject('Oops!'))
        moan.task('default', 'bad')

        moan.run()
          .then(() => {
            expect().fail('Should have been rejected')
          })
          .catch(() => {
            expect(moan.currentTask).to.be('bad')
          })
          .then(done, done)
      })
    })

    context('when tasks still in progress', () => {
      it('should be current task', (done) => {
        moan.task('default', (callback) => {
          setImmediate(callback)
        })

        moan.on('started', () => {
          expect(moan.currentTask).to.be('default')

          done()
        })

        moan.run()
      })
    })
  })

  describe('#dependencies', () => {
    it('should return the dependencies for the tasks', () => {
      let expected = [ 'foo', 'bar' ]

      moan.task('default', expected)

      expect(moan.dependencies('default')).to.eql(expected)
    })

    context('when task has no dependencies', () => {
      it('should return an empty array', () => {
        moan.task('default')

        expect(moan.dependencies('default')).to.eql([])
      })
    })

    context('when task does not exist', () => {
      it('should throw an error', () => {
        expect(moan.dependencies.bind(moan)).withArgs('default').to.throwError()
      })
    })

    it('should not allow modifications', () => {
      let expected = [ 'foo', 'bar' ]

      moan.task('default', expected)
      moan.dependencies('default').push('fu')

      expect(moan.dependencies('default')).to.eql(expected)
    })
  })

  describe('#failed', () => {
    context('when task has failed to complete', () => {
      it('should return true', (done) => {
        moan.task('default', () => Promise.reject('Oops!'))

        moan.run()
          .then(() => {
            expect().fail('Should have been rejected')
          })
          .catch(() => {
            expect(moan.failed('default')).to.be(true)
          })
          .then(done, done)
      })
    })

    context('when task has completed successfully', () => {
      it('should return false', (done) => {
        moan.task('default', () => Promise.resolve('Yipee!'))

        moan.run()
          .then(() => {
            expect(moan.failed('default')).to.be(false)
          })
          .then(done, done)
      })
    })

    context('when task is not complete', () => {
      it('should return false', () => {
        moan.task('default')

        expect(moan.failed('default')).to.be(false)
      })
    })

    context('when task does not exist', () => {
      it('should throw an error', () => {
        expect(moan.failed.bind(moan)).withArgs('default').to.throwError()
      })
    })
  })

  describe('#fileSet', () => {
    // TODO: Complete unit tests
  })

  describe('#fileSets', () => {
    it('should be contain all unique registered file sets', () => {
      moan.fileSet('foo', [])
      moan.fileSet('bar', [])
      moan.fileSet('foo', [])

      expect(moan.fileSets).to.eql([ 'foo', 'bar' ])
    })

    context('when no file sets have been registered', () => {
      it('should be an empty array', () => {
        expect(moan.fileSets).to.eql([])
      })
    })

    it('should be read-only', () => {
      try {
        moan.fileSets = [ 'foo' ]

        expect().fail('Should have thrown error')
      } catch (error) {
        expect(error).to.be.a(TypeError)
      }
    })

    it('should not allow modifications', () => {
      moan.fileSets.push('foo')

      expect(moan.fileSets).to.eql([])
    })
  })

  describe('#finished', () => {
    context('when task has finished erroneously', () => {
      it('should return true', (done) => {
        moan.task('default', () => Promise.reject('Oops!'))

        moan.run()
          .then(() => {
            expect().fail('Should have been rejected')
          })
          .catch(() => {
            expect(moan.finished('default')).to.be(true)
          })
          .then(done, done)
      })
    })

    context('when task has finished successfully', () => {
      it('should return true', (done) => {
        moan.task('default', () => Promise.resolve('Yipee!'))

        moan.run()
          .then(() => {
            expect(moan.finished('default')).to.be(true)
          })
          .then(done, done)
      })
    })

    context('when task is not complete', () => {
      it('should return false', () => {
        moan.task('default')

        expect(moan.finished('default')).to.be(false)
      })
    })

    context('when task does not exist', () => {
      it('should throw an error', () => {
        expect(moan.finished.bind(moan)).withArgs('default').to.throwError()
      })
    })
  })

  describe('#result', () => {
    context('when synchronous task is finished', () => {
      it('should return result if one is returned', (done) => {
        let expected = 'foo'

        moan.task('default', () => expected)

        moan.run()
          .then(() => {
            expect(moan.result('default')).to.be(expected)
          })
          .then(done, done)
      })

      it('should return nothing if nothing is returned', (done) => {
        moan.task('default', () => {})

        moan.run()
          .then(() => {
            expect(moan.result('default')).not.to.be.ok()
          })
          .then(done, done)
      })
    })

    context('when asynchronous task with a callback function is finished', () => {
      it('should return result if one is passed', (done) => {
        let expected = 'foo'

        moan.task('default', (callback) => {
          callback(null, expected)
        })

        moan.run()
          .then(() => {
            expect(moan.result('default')).to.be(expected)
          })
          .then(done, done)
      })

      it('should return nothing if nothing is passed', (done) => {
        moan.task('default', (callback) => {
          callback(null)
        })

        moan.run()
          .then(() => {
            expect(moan.result('default')).not.to.be.ok()
          })
          .then(done, done)
      })
    })

    context('when asynchronous task with a promise is finished', () => {
      it('should return result if resolved with one', (done) => {
        let expected = 'foo'

        moan.task('default', () => Promise.resolve(expected))

        moan.run()
          .then(() => {
            expect(moan.result('default')).to.be(expected)
          })
          .then(done, done)
      })

      it('should return nothing if nothing is returned', (done) => {
        moan.task('default', () => Promise.resolve())

        moan.run()
          .then(() => {
            expect(moan.result('default')).not.to.be.ok()
          })
          .then(done, done)
      })
    })

    it('should return nothing if task has finished erroneously', (done) => {
      moan.task('default', () => Promise.reject('Oops!'))

      moan.run()
        .then(() => {
          expect().fail('Should have been rejected')
        })
        .catch(() => {
          expect(moan.result('default')).not.to.be.ok()
        })
        .then(done, done)
    })

    it('should throw an error if task does not exist', () => {
      expect(moan.result.bind(moan)).withArgs('default').to.throwError()
    })

    it('should throw an error if task has not finished', () => {
      moan.task('default', () => Promise.resolve('Yipee!'))

      expect(moan.result.bind(moan)).withArgs('default').to.throwError()
    })
  })

  describe('#run', () => {
    // TODO: Complete unit tests
  })

  describe('#started', () => {
    context('when task has finished erroneously', () => {
      it('should return true', (done) => {
        moan.task('default', () => Promise.reject('Oops!'))

        moan.run()
          .then(() => {
            expect().fail('Should have been rejected')
          })
          .catch(() => {
            expect(moan.started('default')).to.be(true)
          })
          .then(done, done)
      })
    })

    context('when task has finished successfully', () => {
      it('should return true', (done) => {
        moan.task('default', () => Promise.resolve('Yipee!'))

        moan.run()
          .then(() => {
            expect(moan.started('default')).to.be(true)
          })
          .then(done, done)
      })
    })

    context('when task has started but not completed (in progress)', () => {
      it('should return true', (done) => {
        moan.task('default', (callback) => {
          setImmediate(callback)
        })

        moan.on('started', () => {
          expect(moan.started('default')).to.be(true)
          expect(moan.finished('default')).to.be(false)

          done()
        })

        moan.run()
      })
    })

    context('when task has not started', () => {
      it('should return false', () => {
        moan.task('default')

        expect(moan.started('default')).to.be(false)
      })
    })

    context('when task does not exist', () => {
      it('should throw an error', () => {
        expect(moan.started.bind(moan)).withArgs('default').to.throwError()
      })
    })
  })

  describe('#task', () => {
    // TODO: Complete unit tests
  })

  describe('#tasks', () => {
    it('should be contain all unique registered tasks', () => {
      moan.task('foo')
      moan.task('bar')
      moan.task('foo')

      expect(moan.tasks).to.eql([ 'foo', 'bar' ])
    })

    context('when no tasks have been registered', () => {
      it('should be an empty array', () => {
        expect(moan.tasks).to.eql([])
      })
    })

    it('should be read-only', () => {
      try {
        moan.tasks = [ 'foo' ]

        expect().fail('Should have thrown error')
      } catch (error) {
        expect(error).to.be.a(TypeError)
      }
    })

    it('should not allow modifications', () => {
      moan.tasks.push('foo')

      expect(moan.tasks).to.eql([])
    })
  })

  describe('#version', () => {
    let spyReadFileSync

    beforeEach(() => {
      spyReadFileSync = sinon.spy(fs, 'readFileSync')
    })

    afterEach(() => {
      spyReadFileSync.restore()
    })

    it('should match the version in the package descriptor', (done) => {
      fs.readFile('package.json', 'utf8', (error, data) => {
        if (error) {
          done(error)
        } else {
          expect(moan.version).to.be(JSON.parse(data).version)

          expect(spyReadFileSync.calledOnce).to.be.ok()

          done()
        }
      })
    })

    it('should cache version read from package descriptor', () => {
      let v1 = moan.version
      let v2 = moan.version

      expect(v1).to.be(v2)
      expect(spyReadFileSync.calledOnce).to.be.ok()
    })

    it('should be read-only', () => {
      try {
        moan.version = 'foo'

        expect().fail('Should have thrown error')
      } catch (error) {
        expect(error).to.be.a(TypeError)
      }
    })
  })
})<commit_msg>added tests to cover moan constructor<commit_after>/*
 * moan
 * https://github.com/neocotic/moan
 *
 * Copyright (c) 2015 Alasdair Mercer
 * Licensed under the MIT license.
 * https://github.com/neocotic/moan/blob/master/LICENSE.md
 */

'use strict'

const EventEmitter = require('events').EventEmitter
const expect = require('expect.js')
const fs = require('fs')
const sinon = require('sinon')

const Logger = require('../lib/logger')
const singleton = require('../lib/moan')
const Moan = singleton.Moan

describe('moan', () => {
  it('should be a singleton instance of Moan', () => {
    expect(singleton).to.be.ok()
    expect(singleton).to.be.a(Moan)
  })
})

describe('Moan', () => {
  let moan

  beforeEach(() => {
    moan = new Moan()
    moan.log = sinon.stub(moan.log)
  })

  it('should extend from EventEmitter', () => {
    expect(moan).to.be.an(EventEmitter)
  })

  describe('#constructor', () => {
    it('should setup class correctly', () => {
      let options = { color: false, debug: true }

      moan = new Moan(options)

      expect(moan.color).to.be(false)
      expect(moan.configs).to.eql([])
      expect(moan.currentTask).not.to.be.ok()
      expect(moan.debug).to.be(true)
      expect(moan.fileSets).to.eql([])
      expect(moan.log).to.be.a(Logger)
      expect(moan.tasks).to.eql([])
    })

    context('when no options are provided', () => {
      it('should use default values', () => {
        moan = new Moan()

        expect(moan.color).to.be(true)
        expect(moan.configs).to.eql([])
        expect(moan.currentTask).not.to.be.ok()
        expect(moan.debug).to.be(false)
        expect(moan.fileSets).to.eql([])
        expect(moan.log).to.be.a(Logger)
        expect(moan.tasks).to.eql([])
      })
    })
  })

  describe('#config', () => {
    context('when only configuration key is provided', () => {
      it('should return configuration value', () => {
        moan.config('foo', 'bar')

        expect(moan.config('foo')).to.be('bar')
      })

      it('should return nothing if key does not exist', () => {
        expect(moan.config('foo')).not.to.be.ok()
      })
    })

    context('when configuration key and value are provided', () => {
      it('should return newly configured value', () => {
        expect(moan.config('foo', 'bar')).to.be('bar')
      })

      it('should replace any previously configured value', () => {
        expect(moan.config('foo', 'bar')).to.be('bar')
        expect(moan.config('foo', 'baz')).to.be('baz')

        expect(moan.config('foo')).to.be('baz')
      })
    })
  })

  describe('#configs', () => {
    it('should be contain all unique configurations', () => {
      moan.config('foo', 'bar')
      moan.config('fu', 'baz')
      moan.config('foo', 'buzz')

      expect(moan.configs).to.eql([ 'foo', 'fu' ])
    })

    context('when nothing has been configured', () => {
      it('should be an empty array', () => {
        expect(moan.configs).to.eql([])
      })
    })

    it('should be read-only', () => {
      try {
        moan.configs = [ 'foo' ]

        expect().fail('Should have thrown error')
      } catch (error) {
        expect(error).to.be.a(TypeError)
      }
    })

    it('should not allow modifications', () => {
      moan.configs.push('foo')

      expect(moan.configs).to.eql([])
    })
  })

  describe('#currentTask', () => {
    context('when idle', () => {
      it('should be nothing', () => {
        expect(moan.currentTask).not.to.be.ok()
      })
    })

    context('when tasks have finished successfully', () => {
      it('should be nothing', (done) => {
        moan.task('good', () => Promise.resolve('Yipee!'))
        moan.task('default', 'good')

        moan.run()
          .then(() => {
            expect(moan.currentTask).not.to.be.ok()
          })
          .then(done, done)
      })
    })

    context('when tasks have finished erroneously', () => {
      it('should be task that failed for debugging purposes', (done) => {
        moan.task('bad', () => Promise.reject('Oops!'))
        moan.task('default', 'bad')

        moan.run()
          .then(() => {
            expect().fail('Should have been rejected')
          })
          .catch(() => {
            expect(moan.currentTask).to.be('bad')
          })
          .then(done, done)
      })
    })

    context('when tasks still in progress', () => {
      it('should be current task', (done) => {
        moan.task('default', (callback) => {
          setImmediate(callback)
        })

        moan.on('started', () => {
          expect(moan.currentTask).to.be('default')

          done()
        })

        moan.run()
      })
    })
  })

  describe('#dependencies', () => {
    it('should return the dependencies for the tasks', () => {
      let expected = [ 'foo', 'bar' ]

      moan.task('default', expected)

      expect(moan.dependencies('default')).to.eql(expected)
    })

    context('when task has no dependencies', () => {
      it('should return an empty array', () => {
        moan.task('default')

        expect(moan.dependencies('default')).to.eql([])
      })
    })

    context('when task does not exist', () => {
      it('should throw an error', () => {
        expect(moan.dependencies.bind(moan)).withArgs('default').to.throwError()
      })
    })

    it('should not allow modifications', () => {
      let expected = [ 'foo', 'bar' ]

      moan.task('default', expected)
      moan.dependencies('default').push('fu')

      expect(moan.dependencies('default')).to.eql(expected)
    })
  })

  describe('#failed', () => {
    context('when task has failed to complete', () => {
      it('should return true', (done) => {
        moan.task('default', () => Promise.reject('Oops!'))

        moan.run()
          .then(() => {
            expect().fail('Should have been rejected')
          })
          .catch(() => {
            expect(moan.failed('default')).to.be(true)
          })
          .then(done, done)
      })
    })

    context('when task has completed successfully', () => {
      it('should return false', (done) => {
        moan.task('default', () => Promise.resolve('Yipee!'))

        moan.run()
          .then(() => {
            expect(moan.failed('default')).to.be(false)
          })
          .then(done, done)
      })
    })

    context('when task is not complete', () => {
      it('should return false', () => {
        moan.task('default')

        expect(moan.failed('default')).to.be(false)
      })
    })

    context('when task does not exist', () => {
      it('should throw an error', () => {
        expect(moan.failed.bind(moan)).withArgs('default').to.throwError()
      })
    })
  })

  describe('#fileSet', () => {
    // TODO: Complete unit tests
  })

  describe('#fileSets', () => {
    it('should be contain all unique registered file sets', () => {
      moan.fileSet('foo', [])
      moan.fileSet('bar', [])
      moan.fileSet('foo', [])

      expect(moan.fileSets).to.eql([ 'foo', 'bar' ])
    })

    context('when no file sets have been registered', () => {
      it('should be an empty array', () => {
        expect(moan.fileSets).to.eql([])
      })
    })

    it('should be read-only', () => {
      try {
        moan.fileSets = [ 'foo' ]

        expect().fail('Should have thrown error')
      } catch (error) {
        expect(error).to.be.a(TypeError)
      }
    })

    it('should not allow modifications', () => {
      moan.fileSets.push('foo')

      expect(moan.fileSets).to.eql([])
    })
  })

  describe('#finished', () => {
    context('when task has finished erroneously', () => {
      it('should return true', (done) => {
        moan.task('default', () => Promise.reject('Oops!'))

        moan.run()
          .then(() => {
            expect().fail('Should have been rejected')
          })
          .catch(() => {
            expect(moan.finished('default')).to.be(true)
          })
          .then(done, done)
      })
    })

    context('when task has finished successfully', () => {
      it('should return true', (done) => {
        moan.task('default', () => Promise.resolve('Yipee!'))

        moan.run()
          .then(() => {
            expect(moan.finished('default')).to.be(true)
          })
          .then(done, done)
      })
    })

    context('when task is not complete', () => {
      it('should return false', () => {
        moan.task('default')

        expect(moan.finished('default')).to.be(false)
      })
    })

    context('when task does not exist', () => {
      it('should throw an error', () => {
        expect(moan.finished.bind(moan)).withArgs('default').to.throwError()
      })
    })
  })

  describe('#result', () => {
    context('when synchronous task is finished', () => {
      it('should return result if one is returned', (done) => {
        let expected = 'foo'

        moan.task('default', () => expected)

        moan.run()
          .then(() => {
            expect(moan.result('default')).to.be(expected)
          })
          .then(done, done)
      })

      it('should return nothing if nothing is returned', (done) => {
        moan.task('default', () => {})

        moan.run()
          .then(() => {
            expect(moan.result('default')).not.to.be.ok()
          })
          .then(done, done)
      })
    })

    context('when asynchronous task with a callback function is finished', () => {
      it('should return result if one is passed', (done) => {
        let expected = 'foo'

        moan.task('default', (callback) => {
          callback(null, expected)
        })

        moan.run()
          .then(() => {
            expect(moan.result('default')).to.be(expected)
          })
          .then(done, done)
      })

      it('should return nothing if nothing is passed', (done) => {
        moan.task('default', (callback) => {
          callback(null)
        })

        moan.run()
          .then(() => {
            expect(moan.result('default')).not.to.be.ok()
          })
          .then(done, done)
      })
    })

    context('when asynchronous task with a promise is finished', () => {
      it('should return result if resolved with one', (done) => {
        let expected = 'foo'

        moan.task('default', () => Promise.resolve(expected))

        moan.run()
          .then(() => {
            expect(moan.result('default')).to.be(expected)
          })
          .then(done, done)
      })

      it('should return nothing if nothing is returned', (done) => {
        moan.task('default', () => Promise.resolve())

        moan.run()
          .then(() => {
            expect(moan.result('default')).not.to.be.ok()
          })
          .then(done, done)
      })
    })

    it('should return nothing if task has finished erroneously', (done) => {
      moan.task('default', () => Promise.reject('Oops!'))

      moan.run()
        .then(() => {
          expect().fail('Should have been rejected')
        })
        .catch(() => {
          expect(moan.result('default')).not.to.be.ok()
        })
        .then(done, done)
    })

    it('should throw an error if task does not exist', () => {
      expect(moan.result.bind(moan)).withArgs('default').to.throwError()
    })

    it('should throw an error if task has not finished', () => {
      moan.task('default', () => Promise.resolve('Yipee!'))

      expect(moan.result.bind(moan)).withArgs('default').to.throwError()
    })
  })

  describe('#run', () => {
    // TODO: Complete unit tests
  })

  describe('#started', () => {
    context('when task has finished erroneously', () => {
      it('should return true', (done) => {
        moan.task('default', () => Promise.reject('Oops!'))

        moan.run()
          .then(() => {
            expect().fail('Should have been rejected')
          })
          .catch(() => {
            expect(moan.started('default')).to.be(true)
          })
          .then(done, done)
      })
    })

    context('when task has finished successfully', () => {
      it('should return true', (done) => {
        moan.task('default', () => Promise.resolve('Yipee!'))

        moan.run()
          .then(() => {
            expect(moan.started('default')).to.be(true)
          })
          .then(done, done)
      })
    })

    context('when task has started but not completed (in progress)', () => {
      it('should return true', (done) => {
        moan.task('default', (callback) => {
          setImmediate(callback)
        })

        moan.on('started', () => {
          expect(moan.started('default')).to.be(true)
          expect(moan.finished('default')).to.be(false)

          done()
        })

        moan.run()
      })
    })

    context('when task has not started', () => {
      it('should return false', () => {
        moan.task('default')

        expect(moan.started('default')).to.be(false)
      })
    })

    context('when task does not exist', () => {
      it('should throw an error', () => {
        expect(moan.started.bind(moan)).withArgs('default').to.throwError()
      })
    })
  })

  describe('#task', () => {
    // TODO: Complete unit tests
  })

  describe('#tasks', () => {
    it('should be contain all unique registered tasks', () => {
      moan.task('foo')
      moan.task('bar')
      moan.task('foo')

      expect(moan.tasks).to.eql([ 'foo', 'bar' ])
    })

    context('when no tasks have been registered', () => {
      it('should be an empty array', () => {
        expect(moan.tasks).to.eql([])
      })
    })

    it('should be read-only', () => {
      try {
        moan.tasks = [ 'foo' ]

        expect().fail('Should have thrown error')
      } catch (error) {
        expect(error).to.be.a(TypeError)
      }
    })

    it('should not allow modifications', () => {
      moan.tasks.push('foo')

      expect(moan.tasks).to.eql([])
    })
  })

  describe('#version', () => {
    let spyReadFileSync

    beforeEach(() => {
      spyReadFileSync = sinon.spy(fs, 'readFileSync')
    })

    afterEach(() => {
      spyReadFileSync.restore()
    })

    it('should match the version in the package descriptor', (done) => {
      fs.readFile('package.json', 'utf8', (error, data) => {
        if (error) {
          done(error)
        } else {
          expect(moan.version).to.be(JSON.parse(data).version)

          expect(spyReadFileSync.calledOnce).to.be.ok()

          done()
        }
      })
    })

    it('should cache version read from package descriptor', () => {
      let v1 = moan.version
      let v2 = moan.version

      expect(v1).to.be(v2)
      expect(spyReadFileSync.calledOnce).to.be.ok()
    })

    it('should be read-only', () => {
      try {
        moan.version = 'foo'

        expect().fail('Should have thrown error')
      } catch (error) {
        expect(error).to.be.a(TypeError)
      }
    })
  })
})<|endoftext|>


<commit_before>'use strict';
const Effects = require('./effects.js').Effects;
const util = require('util');
const move = require('./commands').Commands.move;
const CommandUtil = require('./command_util').CommandUtil;

const l10n_dir = __dirname + '/../l10n/skills/';
let l10ncache = {};

/**
 * Localization helper
 * @return string
 */
const L = function(locale, cls, key /*, args... */ ) {
  var l10nFile = l10n_dir + cls + '.yml';
  var l10n = l10ncache[cls + locale] || require('./l10n')(l10nFile);
  l10n.setLocale(locale);
  return l10n.translate.apply(null, [].slice.call(arguments).slice(2));
};

// For activate functions:
// Command event passes in player, args, rooms, npcs, players.

exports.Skills = {

  dual: {
    id: "dual",
    cost: 2,
    name: "Dual Wield",
    description: "Your ability to use two (or more...) weapons at once.",
    usage: "Wield two one-handed weapons. Enjoy.",
    attribute: "quickness",
    type: "passive",
    activate: player => util.log(player.getName() + ' can dual wield.'),
  },

  athletics: {
    id: "athletics",
    cost: 1,
    name: "Athletics",
    description: "You recover more quickly from physical exertion.",
    usage: "Try resting or meditating to recuperate your energy.",
    attribute: "stamina",
    type: "passive",
    activate: () => {},
  },

  concentration: {
    id: "concentration",
    cost: 1,
    name: "Concentration",
    description: "You recover more quickly from mental exertion.",
    usage: "Try meditating to increase focus and reduce stress.",
    attribute: "willpower",
    type: "passive",
    activate: () => {},
  },

  recovery: {
    id: "recovery",
    cost: 1,
    name: "Recovery",
    description: "Your wounds heal more quickly.",
    usage: "Try resting to recover from wounds or ailments.",
    attribute: "stamina",
    type: "passive",
    activate: () => {},
  },

  //// Cleverness-related skills.
  pick: {
    id: "pick",
    cost: 2,
    name: "Lockpick",
    description: "Your ability to illicitly open locked doors or containers.",
    usage: "`pick [exit]`",
    attribute: "cleverness",
    type: "active",
    activate: (player, target, rooms, npcs, players) => {
      if (target) {
        const room = rooms.getAt(player.getLocation());
        const exits = room.getExits();
        const name = player.getName();
        const possibleTargets = exits
          .filter(e => e.direction.indexOf(target.toLowerCase()) > -1);

        util.log(name + ' is trying to pick a lock...');

        if (possibleTargets && possibleTargets.length === 1) {
          const exit = possibleTargets[0];
          const isDoor = exit.hasOwnProperty('door');
          const isLocked = isDoor && exit.door.locked;

          if (isLocked) {
            player.say("<yellow>You attempt to unlock the door...</yellow>");
            const lockpicking = player.getSkills('pick') + player.getAttribute('cleverness');
            const challenge = parseInt(exit.door.difficulty || 10, 10);
            const getExitDesc = locale => rooms.getAt(exit.location).getTitle(locale);

            if (lockpicking > challenge){
              player.say("<bold><cyan>You unlock the door!<cyan></bold>");
              players.eachIf(
                p => CommandUtil.inSameRoom(player, p),
                p => p.say(name + ' swiftly picks the lock to ' + getExitDesc(p.getLocale()) + '.')
              );
              exit.door.locked = false;
              move(exit, player, true);
            } else {
              util.log(name + " fails to pick lock.");
              player.say("<red>You fail to unlock the door.</red>");
              players.eachIf(
                p => CommandUtil.inSameRoom(player, p),
                p => p.say(name + ' tries to unlock the door to ' + getExitDesc(p.getLocale()) +', but fails to pick it.')
              );
              return;
            }
          } else if (isDoor) {
            player.say("That door is not locked.");
            return;
          } else {
            player.say("There is no door in that direction.");
            return;
          }
        } else if (possibleTarget.length) {
          player.say("Which door's lock do you want to pick?");
          return;
        } else {
          player.say("There doesn't seem to be an exit in that direction, much less a lock to pick.");
          return;
        }
      } else {
        player.say("Which door's lock do you want to pick?");
        return;
      }
    },
  }
};
<commit_msg>Add dodge skill.<commit_after>'use strict';
const Effects = require('./effects.js').Effects;
const util = require('util');
const move = require('./commands').Commands.move;
const CommandUtil = require('./command_util').CommandUtil;

const l10n_dir = __dirname + '/../l10n/skills/';
let l10ncache = {};

/**
 * Localization helper
 * @return string
 */
const L = function(locale, cls, key /*, args... */ ) {
  var l10nFile = l10n_dir + cls + '.yml';
  var l10n = l10ncache[cls + locale] || require('./l10n')(l10nFile);
  l10n.setLocale(locale);
  return l10n.translate.apply(null, [].slice.call(arguments).slice(2));
};

// For activate functions:
// Command event passes in player, args, rooms, npcs, players.

exports.Skills = {

  //// Passive skills
  dual: {
    id: "dual",
    cost: 2,
    name: "Dual Wield",
    description: "Your ability to deftly xuse two (or more...) weapons at once.",
    usage: "Wield two one-handed weapons. Enjoy.",
    attribute: "quickness",
    type: "passive",
    activate: player => util.log(player.getName() + ' can dual wield.'),
  },

  athletics: {
    id: "athletics",
    cost: 1,
    name: "Athletics",
    description: "You recover more quickly from physical exertion.",
    usage: "Try resting or meditating to recuperate your energy.",
    attribute: "stamina",
    type: "passive",
    activate: () => {},
  },

  concentration: {
    id: "concentration",
    cost: 1,
    name: "Concentration",
    description: "You recover more quickly from mental exertion.",
    usage: "Try meditating to increase focus and reduce stress.",
    attribute: "willpower",
    type: "passive",
    activate: () => {},
  },

  recovery: {
    id: "recovery",
    cost: 1,
    name: "Recovery",
    description: "Your wounds heal more quickly.",
    usage: "Try resting to recover from wounds or ailments.",
    attribute: "stamina",
    type: "passive",
    activate: () => {},
  },

  dodging: {
    id: "dodging",
    cost: 1,
    name: "Dodging",
    description: "The ability to avoid getting hit.",
    usage: "When in combat, use `stance precise` or `stance cautious`.",
    attribute: "quickness",
    type: "passive",
    activate: () => {},
  },

  //// Active skills
  pick: {
    id: "pick",
    cost: 2,
    name: "Lockpick",
    description: "Your ability to illicitly open locked doors or containers.",
    usage: "`pick [exit]`",
    attribute: "cleverness",
    type: "active",
    activate: (player, target, rooms, npcs, players) => {
      if (target) {
        const room = rooms.getAt(player.getLocation());
        const exits = room.getExits();
        const name = player.getName();
        const possibleTargets = exits
          .filter(e => e.direction.indexOf(target.toLowerCase()) > -1);

        util.log(name + ' is trying to pick a lock...');

        if (possibleTargets && possibleTargets.length === 1) {
          const exit = possibleTargets[0];
          const isDoor = exit.hasOwnProperty('door');
          const isLocked = isDoor && exit.door.locked;

          if (isLocked) {
            player.say("<yellow>You attempt to unlock the door...</yellow>");
            const lockpicking = player.getSkills('pick') + player.getAttribute('cleverness');
            const challenge = parseInt(exit.door.difficulty || 10, 10);
            const getExitDesc = locale => rooms.getAt(exit.location).getTitle(locale);

            if (lockpicking > challenge){
              player.say("<bold><cyan>You unlock the door!<cyan></bold>");
              players.eachIf(
                p => CommandUtil.inSameRoom(player, p),
                p => p.say(name + ' swiftly picks the lock to ' + getExitDesc(p.getLocale()) + '.')
              );
              exit.door.locked = false;
              move(exit, player, true);
            } else {
              util.log(name + " fails to pick lock.");
              player.say("<red>You fail to unlock the door.</red>");
              players.eachIf(
                p => CommandUtil.inSameRoom(player, p),
                p => p.say(name + ' tries to unlock the door to ' + getExitDesc(p.getLocale()) +', but fails to pick it.')
              );
              return;
            }
          } else if (isDoor) {
            player.say("That door is not locked.");
            return;
          } else {
            player.say("There is no door in that direction.");
            return;
          }
        } else if (possibleTarget.length) {
          player.say("Which door's lock do you want to pick?");
          return;
        } else {
          player.say("There doesn't seem to be an exit in that direction, much less a lock to pick.");
          return;
        }
      } else {
        player.say("Which door's lock do you want to pick?");
        return;
      }
    },
  }
};
<|endoftext|>


<commit_before>'use strict'

// process.stdout.write('\033c') //eslint-disableore-line
console.log('start!')
var Hub = require('../../lib')
var fs = require('fs')
var colors = require('colors-browserify')
var http = require('http')
var JSONStream = require('JSONStream')
var hub = new Hub({ //eslint-disable-line
  adapter: {
    inject: require('../../lib/protocol/websocket'),
    id: 'mtv',
    websocket: {

      // val: 'ws://youzi.local:3032'
    }
  },
  shows: {},
  discover: {
    carousel: {},
    lists: {
      free: {},
      recommended: {},
      popular: {},
      new:{}
    }
  },
  channels: {}
  // leveldb: 'mtv',
  // datafromjson: false
})

var https = require('https')

function getImgBase64 (show) {
  getImgBase64nest(show)
  // ff wat beter concurrency handlen
  // show.seasons.each((p) => {
  //   p.episodes.each(function (pr) {
  //     getImgBase64nest(pr)
  //   })
  // })
}

function getImgBase64nest (show) {
  console.log('lezzgo!', show.img && show.img.val)
  if (!show.img|| !show.img.val) {
    return
  }
   https.request({
    host: 'imgmtvplay-a.akamaihd.net',
    path: '/image/4/4?url=http://images.mtvnn.com/' + show.img.val + '/original',
    port: 443,
    method: 'get',
    headers: {
      accepts: '*/*'
    }
  }, function (res) {
    var str = ''
    res.on('data', function (data) {
      str += data.toString('base64')
      // console.log('yo lil data', arguments)
    })
    res.on('end', function (err, data) {
      if (!err && str) {
        show.set({ thumb: str })
      }
    })
  }).end()
}

//var ret = `https://imgmtvplay-a.akamaihd.net/image/20/20?url=http://images.mtvnn.com/${val}/original`
console.log('here!')
if (!hub.datafromjson || hub.datafromjson.val !== true) {
  setTimeout(function () {
    console.log('start loading!'.magenta)

    var count = 0

    var res = fs.createReadStream(__dirname + '/sbs.json')
    // http.request({
    //   host: 'scraper.dev.vigour.io',
    //   path: '/new.json',
    //   port: 80,
    //   method: 'get',
    //   headers: {
    //     accepts: '*/*'
    //   }
    // }, function (res) {
      // load channels
      res.pipe(JSONStream.parse('channels.*'))
      .on('data', function (data) {
        if (data.id) {
          console.log('channel from json:'.green, data.id, data.img)
          hub.channels.set({ [data.id]: data })
        } else {
          console.log('channel from json --> no id:'.red, data)
        }
      })

      // load shows
      res.pipe(JSONStream.parse('shows.*'))
      .on('data', function (data) {
        if (data.id) {
          console.log('show from json:'.blue, data.id, data.img)
          // event ofc
          hub.shows.set({ [data.id.val]: data })
          // xxxxxx
          // var a = new Observable()
          hub.shows[data.id.val].set({
            // currentEpisode: hub.shows[data.id].seasons[0].episodes[0],
            // currentSeason: hub.shows[data.id].seasons[0]
          })

          hub.shows[data.id.val].seasons.each((p) => {
            p.episodes.each((p) => {
              p.set({ time: Math.random() })
              // p.set({
              //   video: p.video.
              // })
            })
          })
          var show = hub.shows[data.id.val]
          show.seasons.each(function (season) {
            let cnt = 1
            if (!show.currentSeason) {
              show.setKey('currentSeason', season)
            }
            season.episodes.each(function (episode) {
              //101706_ed3db47fa8a8f621fc56e05a669523ae
              //"dash": "https://s3-eu-west-1.amazonaws.com/sbs-storage-dev/output/101706_ed3db47fa8a8f621fc56e05a669523ae/101706.mpd"

              if (!show.currentEpisode) {
                show.setKey('currentEpisode', episode)
              }
              // console.log(episode.video.dash.val.replace('\.mpd$', ''))
              episode.set({
                time: Math.random(),
                number: cnt++,
                video: 'https://s3-eu-west-1.amazonaws.com/sbs-storage-dev/output/104698_62f9febd21d06444f05e3ae7c7589a6d/'
                // video: episode.video ? episode.video.dash.val.replace('\.mpd$', '') : 'wtf kak'
              })
            })
          })
          // getImgBase64(hub.shows[data.id])

          count++
          if (count <= 3) {
            hub.discover.carousel.set({
              [data.id]: data
            })
          } else if (count <= 10) {
            hub.discover.lists.free.set({
              [data.id]: data
            })
          } else if (count <= 15) {
            hub.discover.lists.recommended.set({
              [data.id]: data
            })
          } else if (count <= 20) {
            hub.discover.lists.popular.set({
              [data.id]: data
            })
          } else if (count <= 25) {
            hub.discover.lists.new.set({
              [data.id]: data
            })
          }
        } else {
          console.log('show from json --> no id:', data)
        }
      })
      .on('end', function () {
        console.log('LOADED!'.magenta)

        hub.setKey('datafromjson', true)

        hub.adapter.websocket.server.val = 3032
      })
    // }).end()

  }, 1000)
} else {
  console.log('allready got data from the server')
}
<commit_msg>t Merge branch 'feature/Revolution' of github.com:vigour-io/hub into feature/Revolution<commit_after>'use strict'

// process.stdout.write('\033c') //eslint-disableore-line
console.log('start!')
var Hub = require('../../lib')
var fs = require('fs')

var JSONStream = require('JSONStream')
var hub = new Hub({ //eslint-disable-line
  adapter: {
    inject: require('../../lib/protocol/websocket'),
    id: 'mtv',
    websocket: {}
  },
  shows: {},
  discover: {
    carousel: {},
    lists: {
      free: {},
      recommended: {},
      popular: {},
      new: {}
    }
  },
  channels: {}
  // leveldb: 'mtv',
  // datafromjson: false
})

var https = require('https')

function getImgBase64(show) {
  getImgBase64nest(show)
    // ff wat beter concurrency handlen
    // show.seasons.each((p) => {
    //   p.episodes.each(function (pr) {
    //     getImgBase64nest(pr)
    //   })
    // })
}

function getImgBase64nest(show) {
  console.log('lezzgo!', show.img && show.img.val)
  if (!show.img || !show.img.val) {
    return
  }
  https.request({
    host: 'imgmtvplay-a.akamaihd.net',
    path: '/image/4/4?url=http://images.mtvnn.com/' + show.img.val + '/original',
    port: 443,
    method: 'get',
    headers: {
      accepts: '*/*'
    }
  }, function (res) {
    var str = ''
    res.on('data', function (data) {
      str += data.toString('base64')
        // console.log('yo lil data', arguments)
    })
    res.on('end', function (err, data) {
      if (!err && str) {
        show.set({
          thumb: str
        })
      }
    })
  }).end()
}

//var ret = `https://imgmtvplay-a.akamaihd.net/image/20/20?url=http://images.mtvnn.com/${val}/original`
console.log('here!')
if (!hub.datafromjson || hub.datafromjson.val !== true) {
  setTimeout(function () {
    console.log('start loading!'.magenta)

    var count = 0

    var res = fs.createReadStream(__dirname + '/sbs.json')
      // http.request({
      //   host: 'scraper.dev.vigour.io',
      //   path: '/new.json',
      //   port: 80,
      //   method: 'get',
      //   headers: {
      //     accepts: '*/*'
      //   }
      // }, function (res) {
      // load channels
    res.pipe(JSONStream.parse('channels.*'))
      .on('data', function (data) {
        if (data.id) {
          console.log('channel from json:'.green, data.id, data.img)
          hub.channels.set({
            [data.id]: data
          })
        } else {
          console.log('channel from json --> no id:'.red, data)
        }
      })

    // load shows
    res.pipe(JSONStream.parse('shows.*'))
      .on('data', function (data) {
        if (data.id) {
          // event ofc
          hub.shows.setKey(data.id.val, data)
          let show = hub.shows[data.id.val]
          let seasonCount = 1
          let episodeCount = 1
          show.seasons.each(function (season) {
            let cnt = 1
            if (!show.currentSeason) {
              show.setKey('currentSeason', season)
            }
            season.setKey('number', seasonCount++)
            season.episodes.each(function (episode) {
              //101706_ed3db47fa8a8f621fc56e05a669523ae
              //"dash": "https://s3-eu-west-1.amazonaws.com/sbs-storage-dev/output/101706_ed3db47fa8a8f621fc56e05a669523ae/101706.mpd"

              // hls:   https://s3-eu-west-1.amazonaws.com/sbs-storage-dev/output/104698_62f9febd21d06444f05e3ae7c7589a6d/m3u8s/104698.m3u8
              // mpd: https://s3-eu-west-1.amazonaws.com/sbs-storage-dev/output/104698_62f9febd21d06444f05e3ae7c7589a6d/mpds/104698.mpd
              if (!show.currentEpisode) {
                show.setKey('currentEpisode', episode)
              }
              episode.set({
                time: Math.random(),
                number: cnt++,
                video: 'https://s3-eu-west-1.amazonaws.com/sbs-storage-dev/output/104698_62f9febd21d06444f05e3ae7c7589a6d/{type}s/104698.{type}',
                // img: data.img.val
              })
              episodeCount++
            })
            show.set({
              seasonCount: seasonCount,
              episodeCount: episodeCount
            })
          })
            // getImgBase64(hub.shows[data.id])

          count++
          if (count <= 3) {
            hub.discover.carousel.set({
              [data.id]: data
            })
          } else if (count <= 10) {
            hub.discover.lists.free.set({
              [data.id]: data
            })
          } else if (count <= 15) {
            hub.discover.lists.recommended.set({
              [data.id]: data
            })
          } else if (count <= 20) {
            hub.discover.lists.popular.set({
              [data.id]: data
            })
          } else if (count <= 25) {
            hub.discover.lists.new.set({
              [data.id]: data
            })
          }
        } else {
          console.log('show from json --> no id:', data)
        }
      })
      .on('end', function () {
        console.log('LOADED!'.magenta)

        hub.setKey('datafromjson', true)

        hub.adapter.websocket.server.val = 3032
      })
      // }).end()

  }, 1000)
} else {
  console.log('allready got data from the server')
}
<|endoftext|>


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

/**
 * @fileoverview End-to-end tests of the interaction between the player and
 * editor.
 */

var general = require('../protractor_utils/general.js');
var interactions = require('../../../extensions/interactions/protractor.js');
var forms = require('../protractor_utils/forms.js');
var users = require('../protractor_utils/users.js');
var workflow = require('../protractor_utils/workflow.js');
var editor = require('../protractor_utils/editor.js');
var ExplorationPlayerPage =
  require('../protractor_utils/ExplorationPlayerPage.js');

describe('State editor', function() {
  var explorationPlayerPage = null;

  beforeEach(function() {
    explorationPlayerPage = new ExplorationPlayerPage.ExplorationPlayerPage();
  });

  it('should walk through the tutorial when user repeatedly clicks Next',
    function() {
      var NUM_TUTORIAL_STAGES = 7;
      users.createUser(
        'userTutorial@stateEditor.com', 'userTutorialStateEditor');
      users.login('userTutorial@stateEditor.com');

      workflow.createExplorationAndStartTutorial();
      editor.startTutorial();
      for (var i = 0; i < NUM_TUTORIAL_STAGES - 1; i++) {
        editor.progressInTutorial();
        general.waitForSystem();
      }
      editor.finishTutorial();
      users.logout();
    }
  );

  it('should display plain text content', function() {
    users.createUser('user1@stateEditor.com', 'user1StateEditor');
    users.login('user1@stateEditor.com');

    workflow.createExploration();
    editor.setContent(forms.toRichText('plain text'));
    editor.setInteraction('Continue', 'click here');
    editor.setDefaultOutcome(null, 'final card', true);

    // Setup a terminating state
    editor.moveToState('final card');
    editor.setInteraction('EndExploration');
    editor.saveChanges();

    general.moveToPlayer();
    explorationPlayerPage.expectContentToMatch(forms.toRichText('plain text'));
    explorationPlayerPage.expectExplorationToNotBeOver();
    explorationPlayerPage.expectInteractionToMatch('Continue', 'click here');
    explorationPlayerPage.submitAnswer('Continue', null);
    explorationPlayerPage.expectExplorationToBeOver();

    users.logout();
  });

  it('should create content and multiple choice interactions', function() {
    users.createUser('user2@stateEditor.com', 'user2StateEditor');
    users.login('user2@stateEditor.com');
    workflow.createExploration();
    editor.setContent(function(richTextEditor) {
      richTextEditor.appendBoldText('bold text');
      richTextEditor.appendPlainText(' ');
      richTextEditor.appendItalicText('italic text');
      richTextEditor.appendPlainText(' ');
      richTextEditor.appendPlainText(' ');
      richTextEditor.appendOrderedList(['entry 1', 'entry 2']);
      richTextEditor.appendUnorderedList(['an entry', 'another entry']);
    });
    editor.setInteraction(
      'MultipleChoiceInput',
      [forms.toRichText('option A'), forms.toRichText('option B')]);
    editor.setDefaultOutcome(null, 'final card', true);

    // Setup a terminating state
    editor.moveToState('final card');
    editor.setInteraction('EndExploration');
    editor.saveChanges();

    general.moveToPlayer();
    explorationPlayerPage.expectExplorationToNotBeOver();
    explorationPlayerPage.expectInteractionToMatch(
      'MultipleChoiceInput',
      [forms.toRichText('option A'), forms.toRichText('option B')]);
    explorationPlayerPage.submitAnswer('MultipleChoiceInput', 'option B');
    explorationPlayerPage.expectExplorationToBeOver();
    users.logout();
  });

  it('should obey numeric interaction rules and display feedback', function() {
    users.createUser('user3@stateEditor.com', 'user3StateEditor');
    users.login('user3@stateEditor.com');

    workflow.createExploration();
    editor.setContent(forms.toRichText('some content'));
    editor.setInteraction('NumericInput');
    editor.addResponse('NumericInput', function(richTextEditor) {
      richTextEditor.appendBoldText('correct');
    }, 'final card', true, 'IsInclusivelyBetween', -1, 3);
    editor.setDefaultOutcome(forms.toRichText('out of bounds'), null, false);

    // Setup a terminating state
    editor.moveToState('final card');
    editor.setInteraction('EndExploration');
    editor.saveChanges();

    general.moveToPlayer();
    explorationPlayerPage.submitAnswer('NumericInput', 5);
    explorationPlayerPage.expectLatestFeedbackToMatch(
      forms.toRichText('out of bounds')
    );
    explorationPlayerPage.expectExplorationToNotBeOver();
    // It's important to test the value 0 in order to ensure that it would
    // still get submitted even though it is a falsy value in JavaScript.
    explorationPlayerPage.submitAnswer('NumericInput', 0);
    explorationPlayerPage.expectLatestFeedbackToMatch(
      function(richTextChecker) {
        richTextChecker.readBoldText('correct');
      });
    explorationPlayerPage.clickThroughToNextCard();
    explorationPlayerPage.expectExplorationToBeOver();

    users.logout();
  });

  it('should skip the customization modal for interactions having no ' +
      'customization options', function() {
    users.createUser('user4@stateEditor.com', 'user4StateEditor');
    users.login('user4@stateEditor.com');

    workflow.createExploration();
    editor.setContent(forms.toRichText('some content'));

    // Numeric input does not have any customization arguments. Therefore the
    // customization modal and the save interaction button does not appear.
    editor.openInteraction('NumericInput');
    var saveInteractionBtn = element(
      by.css('.protractor-test-save-interaction'));
    expect(saveInteractionBtn.isPresent()).toBe(false);

    element(by.css('.protractor-test-close-add-response-modal')).click();

    // The Continue input has customization options. Therefore the
    // customization modal does appear and so does the save interaction button.
    editor.openInteraction('Continue');
    expect(saveInteractionBtn.isPresent()).toBe(true);
    users.logout();
  });

  it('should open appropriate modal on re-clicking an interaction to ' +
     'customize it', function() {
    users.createUser('user5@stateEditor.com', 'user5StateEditor');
    users.login('user5@stateEditor.com');

    workflow.createExploration();
    editor.setContent(forms.toRichText('some content'));

    // Numeric input does not have any customization arguments. Therefore, on
    // re-clicking, a modal opens up informing the user that this interaction
    // does not have any customization options. To dismiss this modal, user
    // clicks 'Okay' implying that he/she has got the message.
    editor.setInteraction('NumericInput');
    element(by.css('.protractor-test-interaction')).click();
    var okayBtn = element(
      by.css('.protractor-test-close-no-customization-modal'));
    expect(okayBtn.isPresent()).toBe(true);
    okayBtn.click();

    // Continue input has customization options. Therefore, on re-clicking, a
    // modal opens up containing the customization arguments for this input.
    // The user can dismiss this modal by clicking the 'Save Interaction'
    // button.
    editor.setInteraction('Continue');
    element(by.css('.protractor-test-interaction')).click();
    var saveInteractionBtn = element(
      by.css('.protractor-test-save-interaction'));
    expect(saveInteractionBtn.isPresent()).toBe(true);
    saveInteractionBtn.click();

    users.logout();
  });

  it('should preserve input value when rule type changes in' +
      ' add response modal', function() {
    users.createUser('stateEditorUser1@example.com', 'stateEditorUser1');
    users.login('stateEditorUser1@example.com');
    workflow.createExploration();
    editor.setContent(forms.toRichText('some content'));

    editor.openInteraction('TextInput');
    editor.customizeInteraction('TextInput', 'My PlaceHolder', 2);
    editor.selectRuleInAddResponseModal('TextInput', 'Equals');
    editor.setRuleParametersInAddResponseModal('TextInput',
      'Equals', 'Some Text');
    editor.expectRuleParametersToBe('TextInput', 'Equals', 'Some Text');
    editor.selectRuleInAddResponseModal('TextInput', 'Contains');
    editor.expectRuleParametersToBe('TextInput', 'Equals', 'Some Text');
    editor.closeAddResponseModal();

    editor.saveChanges();
    users.logout();
  });

  it('should add/modify/delete a hint', function() {
    users.createUser('stateEditorUser2@example.com', 'stateEditorUser2');
    users.login('stateEditorUser2@example.com');
    workflow.createExploration();
    editor.setContent(forms.toRichText('some content'));

    editor.openInteraction('TextInput');
    editor.customizeInteraction('TextInput', 'My PlaceHolder', 2);
    editor.selectRuleInAddResponseModal('TextInput', 'Equals');
    editor.setRuleParametersInAddResponseModal('TextInput',
      'Equals', 'Some Text');
    editor.expectRuleParametersToBe('TextInput', 'Equals', 'Some Text');
    editor.selectRuleInAddResponseModal('TextInput', 'Contains');
    editor.expectRuleParametersToBe('TextInput', 'Equals', 'Some Text');
    editor.closeAddResponseModal();
    editor.addHint('hint one');
    editor.HintEditor(0).setHint('modified hint one');
    editor.HintEditor(0).deleteHint();
    general.waitForSystem();
    editor.saveChanges();
    users.logout();
  });

  it('should add a solution', function() {
    users.login('stateEditorUser1@example.com');
    workflow.createExploration();
    editor.setContent(forms.toRichText('some content'));

    editor.openInteraction('TextInput');
    editor.customizeInteraction('TextInput', 'My PlaceHolder', 2);
    editor.addResponse('TextInput', function(richTextEditor) {
      richTextEditor.appendBoldText('correct');
    }, 'final card', true, 'Equals', 'Some Text');

    editor.addHint('hint one');
    editor.addSolution('TextInput', {
      answerIsExclusive: true,
      correctAnswer: 'Some Text',
      explanation: 'sample explanation'
    });

    editor.saveChanges();
    users.logout();
  });

  afterEach(function() {
    general.checkForConsoleErrors([]);
  });
});
<commit_msg>Fix a flaky protractor test. (#4347)<commit_after>// Copyright 2014 The Oppia Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview End-to-end tests of the interaction between the player and
 * editor.
 */

var general = require('../protractor_utils/general.js');
var interactions = require('../../../extensions/interactions/protractor.js');
var forms = require('../protractor_utils/forms.js');
var users = require('../protractor_utils/users.js');
var workflow = require('../protractor_utils/workflow.js');
var editor = require('../protractor_utils/editor.js');
var ExplorationPlayerPage =
  require('../protractor_utils/ExplorationPlayerPage.js');

describe('State editor', function() {
  var explorationPlayerPage = null;

  beforeEach(function() {
    explorationPlayerPage = new ExplorationPlayerPage.ExplorationPlayerPage();
  });

  it('should walk through the tutorial when user repeatedly clicks Next',
    function() {
      var NUM_TUTORIAL_STAGES = 7;
      users.createUser(
        'userTutorial@stateEditor.com', 'userTutorialStateEditor');
      users.login('userTutorial@stateEditor.com');

      workflow.createExplorationAndStartTutorial();
      editor.startTutorial();
      for (var i = 0; i < NUM_TUTORIAL_STAGES - 1; i++) {
        editor.progressInTutorial();
        general.waitForSystem();
      }
      editor.finishTutorial();
      users.logout();
    }
  );

  it('should display plain text content', function() {
    users.createUser('user1@stateEditor.com', 'user1StateEditor');
    users.login('user1@stateEditor.com');

    workflow.createExploration();
    editor.setContent(forms.toRichText('plain text'));
    editor.setInteraction('Continue', 'click here');
    editor.setDefaultOutcome(null, 'final card', true);

    // Setup a terminating state
    editor.moveToState('final card');
    editor.setInteraction('EndExploration');
    editor.saveChanges();

    general.moveToPlayer();
    explorationPlayerPage.expectContentToMatch(forms.toRichText('plain text'));
    explorationPlayerPage.expectExplorationToNotBeOver();
    explorationPlayerPage.expectInteractionToMatch('Continue', 'click here');
    explorationPlayerPage.submitAnswer('Continue', null);
    explorationPlayerPage.expectExplorationToBeOver();

    users.logout();
  });

  it('should create content and multiple choice interactions', function() {
    users.createUser('user2@stateEditor.com', 'user2StateEditor');
    users.login('user2@stateEditor.com');
    workflow.createExploration();
    editor.setContent(function(richTextEditor) {
      richTextEditor.appendBoldText('bold text');
      richTextEditor.appendPlainText(' ');
      richTextEditor.appendItalicText('italic text');
      richTextEditor.appendPlainText(' ');
      richTextEditor.appendPlainText(' ');
      richTextEditor.appendOrderedList(['entry 1', 'entry 2']);
      richTextEditor.appendUnorderedList(['an entry', 'another entry']);
    });
    editor.setInteraction(
      'MultipleChoiceInput',
      [forms.toRichText('option A'), forms.toRichText('option B')]);
    editor.setDefaultOutcome(null, 'final card', true);

    // Setup a terminating state
    editor.moveToState('final card');
    editor.setInteraction('EndExploration');
    editor.saveChanges();

    general.moveToPlayer();
    explorationPlayerPage.expectExplorationToNotBeOver();
    explorationPlayerPage.expectInteractionToMatch(
      'MultipleChoiceInput',
      [forms.toRichText('option A'), forms.toRichText('option B')]);
    explorationPlayerPage.submitAnswer('MultipleChoiceInput', 'option B');
    explorationPlayerPage.expectExplorationToBeOver();
    users.logout();
  });

  it('should obey numeric interaction rules and display feedback', function() {
    users.createUser('user3@stateEditor.com', 'user3StateEditor');
    users.login('user3@stateEditor.com');

    workflow.createExploration();
    editor.setContent(forms.toRichText('some content'));
    editor.setInteraction('NumericInput');
    editor.addResponse('NumericInput', function(richTextEditor) {
      richTextEditor.appendBoldText('correct');
    }, 'final card', true, 'IsInclusivelyBetween', -1, 3);
    editor.setDefaultOutcome(forms.toRichText('out of bounds'), null, false);

    // Setup a terminating state
    editor.moveToState('final card');
    editor.setInteraction('EndExploration');
    editor.saveChanges();

    general.moveToPlayer();
    explorationPlayerPage.submitAnswer('NumericInput', 5);
    explorationPlayerPage.expectLatestFeedbackToMatch(
      forms.toRichText('out of bounds')
    );
    explorationPlayerPage.expectExplorationToNotBeOver();
    // It's important to test the value 0 in order to ensure that it would
    // still get submitted even though it is a falsy value in JavaScript.
    explorationPlayerPage.submitAnswer('NumericInput', 0);
    explorationPlayerPage.expectLatestFeedbackToMatch(
      function(richTextChecker) {
        richTextChecker.readBoldText('correct');
      });
    explorationPlayerPage.clickThroughToNextCard();
    explorationPlayerPage.expectExplorationToBeOver();

    users.logout();
  });

  it('should skip the customization modal for interactions having no ' +
      'customization options', function() {
    users.createUser('user4@stateEditor.com', 'user4StateEditor');
    users.login('user4@stateEditor.com');

    workflow.createExploration();
    editor.setContent(forms.toRichText('some content'));

    // Numeric input does not have any customization arguments. Therefore the
    // customization modal and the save interaction button does not appear.
    editor.openInteraction('NumericInput');
    var saveInteractionBtn = element(
      by.css('.protractor-test-save-interaction'));
    expect(saveInteractionBtn.isPresent()).toBe(false);

    editor.closeAddResponseModal();

    // The Continue input has customization options. Therefore the
    // customization modal does appear and so does the save interaction button.
    editor.openInteraction('Continue');
    expect(saveInteractionBtn.isPresent()).toBe(true);
    users.logout();
  });

  it('should open appropriate modal on re-clicking an interaction to ' +
     'customize it', function() {
    users.createUser('user5@stateEditor.com', 'user5StateEditor');
    users.login('user5@stateEditor.com');

    workflow.createExploration();
    editor.setContent(forms.toRichText('some content'));

    // Numeric input does not have any customization arguments. Therefore, on
    // re-clicking, a modal opens up informing the user that this interaction
    // does not have any customization options. To dismiss this modal, user
    // clicks 'Okay' implying that he/she has got the message.
    editor.setInteraction('NumericInput');
    element(by.css('.protractor-test-interaction')).click();
    var okayBtn = element(
      by.css('.protractor-test-close-no-customization-modal'));
    expect(okayBtn.isPresent()).toBe(true);
    okayBtn.click();

    // Continue input has customization options. Therefore, on re-clicking, a
    // modal opens up containing the customization arguments for this input.
    // The user can dismiss this modal by clicking the 'Save Interaction'
    // button.
    editor.setInteraction('Continue');
    element(by.css('.protractor-test-interaction')).click();
    var saveInteractionBtn = element(
      by.css('.protractor-test-save-interaction'));
    expect(saveInteractionBtn.isPresent()).toBe(true);
    saveInteractionBtn.click();

    users.logout();
  });

  it('should preserve input value when rule type changes in' +
      ' add response modal', function() {
    users.createUser('stateEditorUser1@example.com', 'stateEditorUser1');
    users.login('stateEditorUser1@example.com');
    workflow.createExploration();
    editor.setContent(forms.toRichText('some content'));

    editor.openInteraction('TextInput');
    editor.customizeInteraction('TextInput', 'My PlaceHolder', 2);
    editor.selectRuleInAddResponseModal('TextInput', 'Equals');
    editor.setRuleParametersInAddResponseModal('TextInput',
      'Equals', 'Some Text');
    editor.expectRuleParametersToBe('TextInput', 'Equals', 'Some Text');
    editor.selectRuleInAddResponseModal('TextInput', 'Contains');
    editor.expectRuleParametersToBe('TextInput', 'Equals', 'Some Text');
    editor.closeAddResponseModal();

    editor.saveChanges();
    users.logout();
  });

  it('should add/modify/delete a hint', function() {
    users.createUser('stateEditorUser2@example.com', 'stateEditorUser2');
    users.login('stateEditorUser2@example.com');
    workflow.createExploration();
    editor.setContent(forms.toRichText('some content'));

    editor.openInteraction('TextInput');
    editor.customizeInteraction('TextInput', 'My PlaceHolder', 2);
    editor.selectRuleInAddResponseModal('TextInput', 'Equals');
    editor.setRuleParametersInAddResponseModal('TextInput',
      'Equals', 'Some Text');
    editor.expectRuleParametersToBe('TextInput', 'Equals', 'Some Text');
    editor.selectRuleInAddResponseModal('TextInput', 'Contains');
    editor.expectRuleParametersToBe('TextInput', 'Equals', 'Some Text');
    editor.closeAddResponseModal();
    editor.addHint('hint one');
    editor.HintEditor(0).setHint('modified hint one');
    editor.HintEditor(0).deleteHint();
    general.waitForSystem();
    editor.saveChanges();
    users.logout();
  });

  it('should add a solution', function() {
    users.login('stateEditorUser1@example.com');
    workflow.createExploration();
    editor.setContent(forms.toRichText('some content'));

    editor.openInteraction('TextInput');
    editor.customizeInteraction('TextInput', 'My PlaceHolder', 2);
    editor.addResponse('TextInput', function(richTextEditor) {
      richTextEditor.appendBoldText('correct');
    }, 'final card', true, 'Equals', 'Some Text');

    editor.addHint('hint one');
    editor.addSolution('TextInput', {
      answerIsExclusive: true,
      correctAnswer: 'Some Text',
      explanation: 'sample explanation'
    });

    editor.saveChanges();
    users.logout();
  });

  afterEach(function() {
    general.checkForConsoleErrors([]);
  });
});
<|endoftext|>


<commit_before>module.exports = function() {
  var queryString = decodeURIComponent(window.location.search);
  var tx = null; // transaction id
  var amt = null;
  var cc = null; // currency code
  var frq = null; // frequency
  var product_name = null;
  var product_category = 'one-time';

  // Search for PayPal Params
  var pp_tx_re = /tx=(\w+)/;
  var pp_amt_re = /amt=([\w\.]+)/;
  var pp_cc_re = /cc=(\w+)/;
  var pp_frequency_re = /frequency=(\w+)/;
  if (pp_tx_re.test(queryString) && pp_amt_re.test(queryString) && pp_cc_re.test(queryString)) {
    tx = queryString.match(pp_tx_re)[1];
    amt = queryString.match(pp_amt_re)[1];
    cc = queryString.match(pp_cc_re)[1];
    product_name = 'PayPal Donation';

    // check for monthly donations
    if (pp_frequency_re.test(queryString)) {
      frq = queryString.match(pp_frequency_re)[1];
      if (frq === 'monthly') {
        product_category = 'monthly';
      }
    }
  }

  // Search for Coinbase Params
  var cb_tx_re = /order\[transaction\]\[id\]=(\w+)/;
  var cb_amt_re = /order\[total_native\]\[cents\]=([\w\.]+)/;
  var cb_cc_re = /order\[total_payout\]\[currency_iso\]=(\w+)/;
  if (cb_tx_re.test(queryString) && cb_amt_re.test(queryString) && cb_cc_re.test(queryString)) {
    tx = queryString.match(cb_tx_re)[1];
    amt = queryString.match(cb_amt_re)[1] / 100; // coinbase amount in cents
    cc = queryString.match(cb_cc_re)[1];
    product_name = 'Bitcoin Donation';
  }

  // Search for Stripe Params
  var str_tx_re = /str_id=(\w+)/;
  var str_amt_re = /str_amount=([\w\.]+)/;
  var str_cc_re = /str_currency=(\w+)/;
  var str_frequency_re = /str_frequency=(\w+)/;
  if (str_tx_re.test(queryString) && str_amt_re.test(queryString) && str_cc_re.test(queryString)) {
    tx = queryString.match(str_tx_re)[1];
    amt = queryString.match(str_amt_re)[1] / 100; // Stripe amount in cents;
    cc = queryString.match(str_cc_re)[1];
    product_name = 'Stripe Donation';

    // check for monthly donations
    if (str_frequency_re.test(queryString)) {
      frq = queryString.match(str_frequency_re)[1];
      if (frq === 'monthly') {
        product_category = 'monthly';
      }
    }
  }

  // increase analytics value of monthly gifts
  if (product_category === 'monthly') {
    // estimate based on YR1 value
    amt = amt * 12;
  }

  /**
   * Function to load JSON file
   * @param  {String} pathToFile
   * @return {JSON}
   */
  function loadJSON(pathToFile) {
    var xhttp = new XMLHttpRequest();
    xhttp.overrideMimeType('application/json');
    xhttp.open('GET', pathToFile, false); // async = false
    xhttp.send();
    if (xhttp.status === 200) {
      var file = xhttp.responseText;
      try {
        return JSON.parse(file);
      } catch(e) {}
    }
    return null;
  }

  // Exchange rates for conversion metrics
  var rates = null;
  if (cc && cc !== 'USD') {
    rates = loadJSON('/api/exchange-rates/latest.json');
  }

  // 2 DP
  function toTwoDP (i) {
    return Math.round(i * 100) / 100;
  }

  if (tx && amt && cc) {

    var exchangeRate = 1;
    var amtUSD = amt;

    if (cc !== 'USD') {
      if (rates && rates.rates && rates.rates[cc]) {
        exchangeRate = rates.rates[cc];
        amtUSD = toTwoDP(amt / exchangeRate);
      }
    }

    // Filter out impact of major gifts on conversion analysis
    // otherwise transactions can skew the average too far
    if (amtUSD > 1000) {
      amtUSD = 1000;
      amt = toTwoDP(1000 / exchangeRate);
    }

    // Optimizely conversion tracking
    window.optimizely = window.optimizely || [];
    window.optimizely.push(['trackEvent', 'donation', {
      // Optimizely isn't multi currency, so only works with USD, in cents
      'revenue': amtUSD * 100
    }]);

    if (window.ga) {

      // We're using universal analytics ecommerce tracking
      // https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce
      ga('require', 'ecommerce');

      ga('ecommerce:addTransaction', {
        'id': tx,
        'affiliation': 'Donations',
        'revenue': amt,
        'shipping': '0',
        'tax': '0',
        'currency': cc
      });

      ga('ecommerce:addItem', {
        'id': tx,
        'name': product_name,
        'sku': product_name,
        'category': product_category,
        'price': amt,
        'quantity': '1'
      });

      //submit transaction to the Analytics servers
      ga('ecommerce:send');

    }
  }
};
<commit_msg>Simplify the logic for the exchange rate<commit_after>var rates = require('../exchange-rates/latest.json');

module.exports = function() {
  var queryString = decodeURIComponent(window.location.search);
  var tx = null; // transaction id
  var amt = null;
  var cc = null; // currency code
  var frq = null; // frequency
  var product_name = null;
  var product_category = 'one-time';

  // Search for PayPal Params
  var pp_tx_re = /tx=(\w+)/;
  var pp_amt_re = /amt=([\w\.]+)/;
  var pp_cc_re = /cc=(\w+)/;
  var pp_frequency_re = /frequency=(\w+)/;
  if (pp_tx_re.test(queryString) && pp_amt_re.test(queryString) && pp_cc_re.test(queryString)) {
    tx = queryString.match(pp_tx_re)[1];
    amt = queryString.match(pp_amt_re)[1];
    cc = queryString.match(pp_cc_re)[1];
    product_name = 'PayPal Donation';

    // check for monthly donations
    if (pp_frequency_re.test(queryString)) {
      frq = queryString.match(pp_frequency_re)[1];
      if (frq === 'monthly') {
        product_category = 'monthly';
      }
    }
  }

  // Search for Coinbase Params
  var cb_tx_re = /order\[transaction\]\[id\]=(\w+)/;
  var cb_amt_re = /order\[total_native\]\[cents\]=([\w\.]+)/;
  var cb_cc_re = /order\[total_payout\]\[currency_iso\]=(\w+)/;
  if (cb_tx_re.test(queryString) && cb_amt_re.test(queryString) && cb_cc_re.test(queryString)) {
    tx = queryString.match(cb_tx_re)[1];
    amt = queryString.match(cb_amt_re)[1] / 100; // coinbase amount in cents
    cc = queryString.match(cb_cc_re)[1];
    product_name = 'Bitcoin Donation';
  }

  // Search for Stripe Params
  var str_tx_re = /str_id=(\w+)/;
  var str_amt_re = /str_amount=([\w\.]+)/;
  var str_cc_re = /str_currency=(\w+)/;
  var str_frequency_re = /str_frequency=(\w+)/;
  if (str_tx_re.test(queryString) && str_amt_re.test(queryString) && str_cc_re.test(queryString)) {
    tx = queryString.match(str_tx_re)[1];
    amt = queryString.match(str_amt_re)[1] / 100; // Stripe amount in cents;
    cc = queryString.match(str_cc_re)[1];
    product_name = 'Stripe Donation';

    // check for monthly donations
    if (str_frequency_re.test(queryString)) {
      frq = queryString.match(str_frequency_re)[1];
      if (frq === 'monthly') {
        product_category = 'monthly';
      }
    }
  }

  // increase analytics value of monthly gifts
  if (product_category === 'monthly') {
    // estimate based on YR1 value
    amt = amt * 12;
  }

  // 2 DP
  function toTwoDP (i) {
    return Math.round(i * 100) / 100;
  }

  if (tx && amt && cc) {

    var exchangeRate = 1;
    var amtUSD = amt;

    if (cc !== 'USD' && rates.rates[cc]) {
      exchangeRate = rates.rates[cc];
      amtUSD = toTwoDP(amt / exchangeRate);
    }

    // Filter out impact of major gifts on conversion analysis
    // otherwise transactions can skew the average too far
    if (amtUSD > 1000) {
      amtUSD = 1000;
      amt = toTwoDP(1000 / exchangeRate);
    }

    // Optimizely conversion tracking
    window.optimizely = window.optimizely || [];
    window.optimizely.push(['trackEvent', 'donation', {
      // Optimizely isn't multi currency, so only works with USD, in cents
      'revenue': amtUSD * 100
    }]);

    if (window.ga) {

      // We're using universal analytics ecommerce tracking
      // https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce
      ga('require', 'ecommerce');

      ga('ecommerce:addTransaction', {
        'id': tx,
        'affiliation': 'Donations',
        'revenue': amt,
        'shipping': '0',
        'tax': '0',
        'currency': cc
      });

      ga('ecommerce:addItem', {
        'id': tx,
        'name': product_name,
        'sku': product_name,
        'category': product_category,
        'price': amt,
        'quantity': '1'
      });

      //submit transaction to the Analytics servers
      ga('ecommerce:send');

    }
  }
};
<|endoftext|>


<commit_before>import Model, { hasMany, belongsTo, attr } from '@ember-data/model';
import moment from 'moment';
import sortableByPosition from 'ilios-common/utils/sortable-by-position';
import striptags from 'striptags';

import { use } from 'ember-could-get-used-to-this';
import ResolveAsyncValue from 'ilios-common/classes/resolve-async-value';
import ResolveFlatMapBy from 'ilios-common/classes/resolve-flat-map-by';
import AsyncProcess from 'ilios-common/classes/async-process';

export default class SessionModel extends Model {
  @attr('string')
  title;

  @attr('string')
  description;

  @attr('boolean')
  attireRequired;

  @attr('boolean')
  equipmentRequired;

  @attr('boolean')
  supplemental;

  @attr('boolean')
  attendanceRequired;

  @attr('string')
  instructionalNotes;

  @attr('date')
  updatedAt;

  @attr('boolean')
  publishedAsTbd;

  @attr('boolean')
  published;

  @belongsTo('session-type', { async: true })
  sessionType;

  @belongsTo('course', { async: true })
  course;

  @belongsTo('ilm-session', { async: true })
  ilmSession;

  @hasMany('session-objective', { async: true })
  sessionObjectives;

  @hasMany('mesh-descriptor', { async: true })
  meshDescriptors;

  @hasMany('session-learning-material', { async: true })
  learningMaterials;

  @hasMany('offering', { async: true })
  offerings;

  @hasMany('user', {
    async: true,
    inverse: 'administeredSessions',
  })
  administrators;

  @hasMany('user', {
    async: true,
    inverse: 'studentAdvisedSessions',
  })
  studentAdvisors;

  @belongsTo('session', {
    inverse: 'prerequisites',
    async: true,
  })
  postrequisite;

  @hasMany('session', {
    inverse: 'postrequisite',
    async: true,
  })
  prerequisites;

  @hasMany('term', { async: true })
  terms;

  @use _offerings = new ResolveAsyncValue(() => [this.offerings]);
  @use offeringLearnerGroups = new ResolveFlatMapBy(() => [this.offerings, 'learnerGroups']);

  get learnerGroupCount() {
    return this.offeringLearnerGroups?.length ?? 0;
  }

  @use _course = new ResolveAsyncValue(() => [this.course]);
  get assignableVocabularies() {
    return this._course?.assignableVocabularies;
  }

  get xObjectives() {
    return this.sessionObjectives;
  }

  @use _ilmSession = new ResolveAsyncValue(() => [this.ilmSession]);
  get isIndependentLearning() {
    return !!this._ilmSession;
  }

  /**
   * All offerings for this session, sorted by offering start date in ascending order.
   */
  get sortedOfferingsByDate() {
    if (!this._offerings) {
      return [];
    }
    const filteredOfferings = this._offerings.filter((offering) => offering.startDate);
    return filteredOfferings.sort((a, b) => {
      const aDate = moment(a.startDate);
      const bDate = moment(b.startDate);
      if (aDate === bDate) {
        return 0;
      }
      return aDate > bDate ? 1 : -1;
    });
  }

  /**
   * The earliest start date of all offerings in this session, or, if this is an ILM session, the ILM's due date.
   */
  get firstOfferingDate() {
    if (this._ilmSession) {
      return this._ilmSession.dueDate;
    }

    if (!this.hasMany('offerings').ids().length) {
      return null;
    }

    return this.sortedOfferingsByDate[0]?.startDate;
  }

  /**
   * The maximum duration in hours (incl. fractions) of any session offerings.
   */
  get maxSingleOfferingDuration() {
    if (!this.hasMany('offerings').ids().length || !this._offerings) {
      return 0;
    }
    const sortedOfferings = this._offerings.toArray().sort(function (a, b) {
      const diffA = moment(a.endDate).diff(moment(a.startDate), 'minutes');
      const diffB = moment(b.endDate).diff(moment(b.startDate), 'minutes');
      if (diffA > diffB) {
        return -1;
      } else if (diffA < diffB) {
        return 1;
      }
      return 0;
    });

    const offering = sortedOfferings[0];
    const duration = moment(offering.endDate).diff(moment(offering.startDate), 'hours', true);

    return duration.toFixed(2);
  }

  /**
   * The total duration in hours (incl. fractions) of all session offerings.
   */
  get totalSumOfferingsDuration() {
    if (!this._offerings?.length) {
      return 0;
    }

    return this._offerings
      .reduce((total, offering) => {
        return total + moment(offering.endDate).diff(moment(offering.startDate), 'hours', true);
      }, 0)
      .toFixed(2);
  }

  /**
   * Total duration in hours for offerings and ILM Sessions
   * If both ILM and offerings are present sum them
   */
  get totalSumDuration() {
    if (!this._ilmSession) {
      return this.totalSumOfferingsDuration;
    }

    const ilmHours = this._ilmSession.hours;

    return parseFloat(ilmHours) + parseFloat(this.totalSumOfferingsDuration);
  }

  /**
   * The maximum duration in hours (incl. fractions) of any session offerings, plus any ILM hours.
   * If both ILM and offerings are present sum them
   */
  get maxDuration() {
    if (!this._ilmSession) {
      return this.maxSingleOfferingDuration;
    }

    const ilmHours = this._ilmSession.hours;

    return parseFloat(ilmHours) + parseFloat(this.maxSingleOfferingDuration);
  }

  @use _allTermVocabularies = new ResolveFlatMapBy(() => [this.terms, 'vocabulary']);
  /**
   * A list of all vocabularies that are associated via terms.
   */
  get associatedVocabularies() {
    return this._allTermVocabularies?.uniq().sortBy('title');
  }

  get termCount() {
    return this.hasMany('terms').ids().length;
  }

  get associatedOfferingLearnerGroups() {
    if (!this.offeringLearnerGroups) {
      return [];
    }
    return this.offeringLearnerGroups.uniq().sortBy('title');
  }
  @use _ilmLearnerGroups = new ResolveAsyncValue(() => [this._ilmSession?.learnerGroups]);
  get associatedIlmLearnerGroups() {
    return this._ilmLearnerGroups?.toArray() ?? [];
  }

  get associatedLearnerGroups() {
    const ilmLearnerGroups = this.isIndependentLearning ? this.associatedIlmLearnerGroups : [];
    if (!this.offeringLearnerGroups || !ilmLearnerGroups) {
      return [];
    }
    return [...this.offeringLearnerGroups, ...ilmLearnerGroups.toArray()].uniq().sortBy('title');
  }

  @use _sessionObjectives = new ResolveAsyncValue(() => [this.sessionObjectives]);
  get sortedSessionObjectives() {
    return this._sessionObjectives?.toArray().sort(sortableByPosition);
  }

  @use _offeringInstructors = new ResolveFlatMapBy(() => [this._offerings, 'instructors']);
  @use _offeringInstructorGroups = new ResolveFlatMapBy(() => [
    this._offerings,
    'instructorGroups',
  ]);
  @use _offeringInstructorGroupInstructors = new ResolveFlatMapBy(() => [
    this._offeringInstructorGroups,
    'users',
  ]);
  @use _ilmSessionInstructors = new ResolveAsyncValue(() => [this._ilmSession?.instructors]);
  @use _ilmSessionInstructorGroups = new ResolveAsyncValue(() => [
    this._ilmSession?.instructorGroups,
  ]);
  @use _ilmSessionInstructorGroupInstructors = new ResolveFlatMapBy(() => [
    this._ilmSessionInstructorGroups,
    'users',
  ]);

  get _ilmSessionInstructorsIfIlmSession() {
    if (!this.isIndependentLearning) {
      return [];
    }

    if (!this._ilmSessionInstructors || !this._ilmSessionInstructorGroupInstructors) {
      return [];
    }

    return [
      ...this._ilmSessionInstructors.toArray(),
      ...this._ilmSessionInstructorGroupInstructors,
    ];
  }

  get allInstructors() {
    if (
      !this._offeringInstructors ||
      !this._offeringInstructorGroupInstructors ||
      !this._ilmSessionInstructorsIfIlmSession
    ) {
      return [];
    }

    return [
      ...this._offeringInstructors,
      ...this._offeringInstructorGroupInstructors,
      ...this._ilmSessionInstructorsIfIlmSession,
    ].uniq();
  }

  get hasPrerequisites() {
    return this.prerequisites.length > 0;
  }

  @use _postrequisite = new ResolveAsyncValue(() => [this.postrequisite]);

  get hasPostrequisite() {
    return !!this._postrequisite;
  }

  @use _sessionObjectiveCourseObjectives = new ResolveFlatMapBy(() => [
    this._sessionObjectives,
    'courseObjectives',
  ]);
  @use showUnlinkIcon = new AsyncProcess(() => [
    this.getShowUnlinkIcon.bind(this),
    this._sessionObjectiveCourseObjectives,
  ]);
  async getShowUnlinkIcon() {
    const sessionObjectives = await this.sessionObjectives;
    const collectionOfCourseObjectives = await Promise.all(
      sessionObjectives.mapBy('courseObjectives')
    );
    return collectionOfCourseObjectives.any((courseObjectives) => courseObjectives.length === 0);
  }

  get requiredPublicationIssues() {
    const issues = [];
    if (this.isIndependentLearning) {
      if (!this._ilmSession?.dueDate) {
        issues.push('dueDate');
      }
    } else {
      if (this.offerings.length === 0) {
        issues.push('offerings');
      }
    }
    if (!this.title) {
      issues.push('title');
    }

    return issues;
  }
  get optionalPublicationIssues() {
    const issues = [];
    if (this.terms.length === 0) {
      issues.push('terms');
    }

    if (this.sessionObjectives.length === 0) {
      issues.push('sessionObjectives');
    }

    if (this.meshDescriptors.length === 0) {
      issues.push('meshDescriptors');
    }

    return issues;
  }

  get isPublished() {
    return this.published;
  }

  get isNotPublished() {
    return !this.isPublished;
  }

  get isScheduled() {
    return this.publishedAsTbd;
  }

  get isPublishedOrScheduled() {
    return this.publishedAsTbd || this.isPublished;
  }

  get allPublicationIssuesLength() {
    return this.requiredPublicationIssues.length + this.optionalPublicationIssues.length;
  }

  get textDescription() {
    return striptags(this.description);
  }
}
<commit_msg>grouping resources, getters, and functions.<commit_after>import Model, { hasMany, belongsTo, attr } from '@ember-data/model';
import moment from 'moment';
import sortableByPosition from 'ilios-common/utils/sortable-by-position';
import striptags from 'striptags';

import { use } from 'ember-could-get-used-to-this';
import ResolveAsyncValue from 'ilios-common/classes/resolve-async-value';
import ResolveFlatMapBy from 'ilios-common/classes/resolve-flat-map-by';
import AsyncProcess from 'ilios-common/classes/async-process';

export default class SessionModel extends Model {
  @attr('string')
  title;

  @attr('string')
  description;

  @attr('boolean')
  attireRequired;

  @attr('boolean')
  equipmentRequired;

  @attr('boolean')
  supplemental;

  @attr('boolean')
  attendanceRequired;

  @attr('string')
  instructionalNotes;

  @attr('date')
  updatedAt;

  @attr('boolean')
  publishedAsTbd;

  @attr('boolean')
  published;

  @belongsTo('session-type', { async: true })
  sessionType;

  @belongsTo('course', { async: true })
  course;

  @belongsTo('ilm-session', { async: true })
  ilmSession;

  @hasMany('session-objective', { async: true })
  sessionObjectives;

  @hasMany('mesh-descriptor', { async: true })
  meshDescriptors;

  @hasMany('session-learning-material', { async: true })
  learningMaterials;

  @hasMany('offering', { async: true })
  offerings;

  @hasMany('user', {
    async: true,
    inverse: 'administeredSessions',
  })
  administrators;

  @hasMany('user', {
    async: true,
    inverse: 'studentAdvisedSessions',
  })
  studentAdvisors;

  @belongsTo('session', {
    inverse: 'prerequisites',
    async: true,
  })
  postrequisite;

  @hasMany('session', {
    inverse: 'postrequisite',
    async: true,
  })
  prerequisites;

  @hasMany('term', { async: true })
  terms;

  @use _offerings = new ResolveAsyncValue(() => [this.offerings]);
  @use offeringLearnerGroups = new ResolveFlatMapBy(() => [this.offerings, 'learnerGroups']);
  @use _course = new ResolveAsyncValue(() => [this.course]);
  @use _ilmSession = new ResolveAsyncValue(() => [this.ilmSession]);
  @use _allTermVocabularies = new ResolveFlatMapBy(() => [this.terms, 'vocabulary']);
  @use _ilmLearnerGroups = new ResolveAsyncValue(() => [this._ilmSession?.learnerGroups]);
  @use _sessionObjectives = new ResolveAsyncValue(() => [this.sessionObjectives]);
  @use _offeringInstructors = new ResolveFlatMapBy(() => [this._offerings, 'instructors']);
  @use _offeringInstructorGroups = new ResolveFlatMapBy(() => [
    this._offerings,
    'instructorGroups',
  ]);
  @use _offeringInstructorGroupInstructors = new ResolveFlatMapBy(() => [
    this._offeringInstructorGroups,
    'users',
  ]);
  @use _ilmSessionInstructors = new ResolveAsyncValue(() => [this._ilmSession?.instructors]);
  @use _ilmSessionInstructorGroups = new ResolveAsyncValue(() => [
    this._ilmSession?.instructorGroups,
  ]);
  @use _ilmSessionInstructorGroupInstructors = new ResolveFlatMapBy(() => [
    this._ilmSessionInstructorGroups,
    'users',
  ]);
  @use _postrequisite = new ResolveAsyncValue(() => [this.postrequisite]);
  @use _sessionObjectiveCourseObjectives = new ResolveFlatMapBy(() => [
    this._sessionObjectives,
    'courseObjectives',
  ]);
  @use showUnlinkIcon = new AsyncProcess(() => [
    this.getShowUnlinkIcon.bind(this),
    this._sessionObjectiveCourseObjectives,
  ]);

  get learnerGroupCount() {
    return this.offeringLearnerGroups?.length ?? 0;
  }

  get assignableVocabularies() {
    return this._course?.assignableVocabularies;
  }

  get xObjectives() {
    return this.sessionObjectives;
  }

  get isIndependentLearning() {
    return !!this._ilmSession;
  }

  /**
   * All offerings for this session, sorted by offering start date in ascending order.
   */
  get sortedOfferingsByDate() {
    if (!this._offerings) {
      return [];
    }
    const filteredOfferings = this._offerings.filter((offering) => offering.startDate);
    return filteredOfferings.sort((a, b) => {
      const aDate = moment(a.startDate);
      const bDate = moment(b.startDate);
      if (aDate === bDate) {
        return 0;
      }
      return aDate > bDate ? 1 : -1;
    });
  }

  /**
   * The earliest start date of all offerings in this session, or, if this is an ILM session, the ILM's due date.
   */
  get firstOfferingDate() {
    if (this._ilmSession) {
      return this._ilmSession.dueDate;
    }

    if (!this.hasMany('offerings').ids().length) {
      return null;
    }

    return this.sortedOfferingsByDate[0]?.startDate;
  }

  /**
   * The maximum duration in hours (incl. fractions) of any session offerings.
   */
  get maxSingleOfferingDuration() {
    if (!this.hasMany('offerings').ids().length || !this._offerings) {
      return 0;
    }
    const sortedOfferings = this._offerings.toArray().sort(function (a, b) {
      const diffA = moment(a.endDate).diff(moment(a.startDate), 'minutes');
      const diffB = moment(b.endDate).diff(moment(b.startDate), 'minutes');
      if (diffA > diffB) {
        return -1;
      } else if (diffA < diffB) {
        return 1;
      }
      return 0;
    });

    const offering = sortedOfferings[0];
    const duration = moment(offering.endDate).diff(moment(offering.startDate), 'hours', true);

    return duration.toFixed(2);
  }

  /**
   * The total duration in hours (incl. fractions) of all session offerings.
   */
  get totalSumOfferingsDuration() {
    if (!this._offerings?.length) {
      return 0;
    }

    return this._offerings
      .reduce((total, offering) => {
        return total + moment(offering.endDate).diff(moment(offering.startDate), 'hours', true);
      }, 0)
      .toFixed(2);
  }

  /**
   * Total duration in hours for offerings and ILM Sessions
   * If both ILM and offerings are present sum them
   */
  get totalSumDuration() {
    if (!this._ilmSession) {
      return this.totalSumOfferingsDuration;
    }

    const ilmHours = this._ilmSession.hours;

    return parseFloat(ilmHours) + parseFloat(this.totalSumOfferingsDuration);
  }

  /**
   * The maximum duration in hours (incl. fractions) of any session offerings, plus any ILM hours.
   * If both ILM and offerings are present sum them
   */
  get maxDuration() {
    if (!this._ilmSession) {
      return this.maxSingleOfferingDuration;
    }

    const ilmHours = this._ilmSession.hours;

    return parseFloat(ilmHours) + parseFloat(this.maxSingleOfferingDuration);
  }

  /**
   * A list of all vocabularies that are associated via terms.
   */
  get associatedVocabularies() {
    return this._allTermVocabularies?.uniq().sortBy('title');
  }

  get termCount() {
    return this.hasMany('terms').ids().length;
  }

  get associatedOfferingLearnerGroups() {
    if (!this.offeringLearnerGroups) {
      return [];
    }
    return this.offeringLearnerGroups.uniq().sortBy('title');
  }
  get associatedIlmLearnerGroups() {
    return this._ilmLearnerGroups?.toArray() ?? [];
  }

  get associatedLearnerGroups() {
    const ilmLearnerGroups = this.isIndependentLearning ? this.associatedIlmLearnerGroups : [];
    if (!this.offeringLearnerGroups || !ilmLearnerGroups) {
      return [];
    }
    return [...this.offeringLearnerGroups, ...ilmLearnerGroups.toArray()].uniq().sortBy('title');
  }

  get sortedSessionObjectives() {
    return this._sessionObjectives?.toArray().sort(sortableByPosition);
  }

  get _ilmSessionInstructorsIfIlmSession() {
    if (!this.isIndependentLearning) {
      return [];
    }

    if (!this._ilmSessionInstructors || !this._ilmSessionInstructorGroupInstructors) {
      return [];
    }

    return [
      ...this._ilmSessionInstructors.toArray(),
      ...this._ilmSessionInstructorGroupInstructors,
    ];
  }

  get allInstructors() {
    if (
      !this._offeringInstructors ||
      !this._offeringInstructorGroupInstructors ||
      !this._ilmSessionInstructorsIfIlmSession
    ) {
      return [];
    }

    return [
      ...this._offeringInstructors,
      ...this._offeringInstructorGroupInstructors,
      ...this._ilmSessionInstructorsIfIlmSession,
    ].uniq();
  }

  get hasPrerequisites() {
    return this.prerequisites.length > 0;
  }

  get hasPostrequisite() {
    return !!this._postrequisite;
  }

  get requiredPublicationIssues() {
    const issues = [];
    if (this.isIndependentLearning) {
      if (!this._ilmSession?.dueDate) {
        issues.push('dueDate');
      }
    } else {
      if (this.offerings.length === 0) {
        issues.push('offerings');
      }
    }
    if (!this.title) {
      issues.push('title');
    }

    return issues;
  }
  get optionalPublicationIssues() {
    const issues = [];
    if (this.terms.length === 0) {
      issues.push('terms');
    }

    if (this.sessionObjectives.length === 0) {
      issues.push('sessionObjectives');
    }

    if (this.meshDescriptors.length === 0) {
      issues.push('meshDescriptors');
    }

    return issues;
  }

  get isPublished() {
    return this.published;
  }

  get isNotPublished() {
    return !this.isPublished;
  }

  get isScheduled() {
    return this.publishedAsTbd;
  }

  get isPublishedOrScheduled() {
    return this.publishedAsTbd || this.isPublished;
  }

  get allPublicationIssuesLength() {
    return this.requiredPublicationIssues.length + this.optionalPublicationIssues.length;
  }

  get textDescription() {
    return striptags(this.description);
  }

  async getShowUnlinkIcon() {
    const sessionObjectives = await this.sessionObjectives;
    const collectionOfCourseObjectives = await Promise.all(
      sessionObjectives.mapBy('courseObjectives')
    );
    return collectionOfCourseObjectives.any((courseObjectives) => courseObjectives.length === 0);
  }
}
<|endoftext|>