<commit_before>import Party from './party.js'
import { Address, Email, PhoneNumber } from '../Children'
import { Comment, Reference } from '../../children'
import PartyLink from '../../children/Link/PartyLink'

describe('Party', () => {
  describe('serialization', () => {
    it('should serialize properly', () => {
      const test = new Party({ partyId: 'test' })
      expect(JSON.parse(JSON.stringify(test)).partyId).toBeDefined()
      expect(JSON.parse(JSON.stringify(test)).partyId).toEqual('test')
    })
  })
  describe('constructor', () => {
    it('should set url', () => {
      const party = new Party({ url: 'testURL' })
      expect(party.url).toEqual('testURL')
    })

    it('should set displayName', () => {
      const party = new Party({ displayName: 'testDisplayName' })
      expect(party.displayName).toEqual('testDisplayName')
    })

    it('should set legalName', () => {
      const party = new Party({ legalName: 'testLegalName' })
      expect(party.legalName).toEqual('testLegalName')
    })

    it('should throw if attempting to set invalid partyStatus', () => {
      let party
      function construct() {
        party = new Party({ partyStatus: 'notAType' })
      }
      expect(construct).toThrowError(
        new Error('Invalid Party Status: notAType')
      )
    })

    it('should set addresses to empty object if class is instantiated without contacts', () => {
      const testParty = new Party({})
      expect(testParty.addresses).toEqual({})
    })

    it('should set references correctly', () => {
      const party = new Party({})
      party.references = {
        INT: { referenceValue: 'Internal1', referencePrimary: true },
        EXT: new Reference({ referenceValue: 'External1' })
      }
      expect(party.references.INT).toBeDefined()
      expect(party.references.INT).toBeInstanceOf(Reference)
      expect(party.references.INT.referenceValue).toEqual('Internal1')
      expect(party.references.INT.referencePrimary).toBeTruthy()
      expect(party.references.EXT).toBeDefined()
      expect(party.references.EXT.referenceValue).toEqual('External1')
      expect(party.references.EXT.referencePrimary).toBeFalsy()
    })

    it('should throw if setting invalid number of primary references', () => {
      const params = {
        references: {
          noPrimary: { referenceValue: '1' }
        }
      }
      const willThrow = () => {
        const party = new Party(params)
      }
      expect(willThrow).toThrowError(
        'Exactly 1 primary Reference must be supplied - found: 0'
      )
    })

    it('should set comments correctly', () => {
      const party = new Party({})
      party.comments = {
        TRADER: { commentValue: 'Strategy1' },
        BACKOFFICE: new Comment({ commentValue: 'Reconciled' })
      }
      expect(party.comments.TRADER).toBeDefined()
      expect(party.comments.TRADER).toBeInstanceOf(Comment)
      expect(party.comments.TRADER.commentValue).toEqual('Strategy1')
      expect(party.comments.BACKOFFICE).toBeDefined()
      expect(party.comments.BACKOFFICE.commentValue).toEqual('Reconciled')
    })

    it('should set links correctly', () => {
      const party = new Party({})
      party.links = {
        SINGLE1: [{ linkedPartyId: 'ID-S1-1' }],
        SINGLE2: [new PartyLink({ linkedPartyId: 'ID-S2-1' })],
        MULTI1: [
          { linkedPartyId: 'ID-M1-1' },
          new PartyLink({ linkedPartyId: 'ID-M1-2' })
        ]
      }
      expect(party.links.SINGLE1[0]).toBeDefined()
      expect(party.links.SINGLE1[0].linkedPartyId).toEqual('ID-S1-1')
      expect(party.links.SINGLE2[0]).toBeDefined()
      expect(party.links.SINGLE2[0].linkedPartyId).toEqual('ID-S2-1')
      expect(party.links.MULTI1[0]).toBeDefined()
      expect(party.links.MULTI1[0].linkedPartyId).toEqual('ID-M1-1')
      expect(party.links.MULTI1[1]).toBeDefined()
      expect(party.links.MULTI1[1].linkedPartyId).toEqual('ID-M1-2')
    })
  })
  describe('addresses', () => {
    it('should set addressPrimary of existing addresses to false when supplying new primary address', () => {
      const primaryOne = new Address({ addressPrimary: true })
      const primaryTwo = new Address({ addressPrimary: true })
      const primaryThree = new Address({ addressPrimary: true })
      const testParty = new Party({
        addresses: { add1: primaryOne, add2: primaryTwo }
      })
      testParty.upsertAddress('add3', primaryThree)
      expect(testParty.addresses.add1.addressPrimary).toBeFalsy()
      expect(testParty.addresses.add2.addressPrimary).toBeFalsy()
    })
    it('should throw if attempting to add primary address without primary address', () => {
      // const primaryOne = new Address({ addressPrimary: true })
      const primaryTwo = new Address({ addressPrimary: false })
      const testParty = new Party({})
      function tester() {
        testParty.upsertAddress('new', primaryTwo)
      }
      expect(tester).toThrowError('At least 1 primary address must be supplied')
    })
    it('should add address and preserve existing ones on upsert', () => {
      const primaryOne = new Address({
        addressPrimary: true,
        lineOne: 'testRoad'
      })
      const primaryTwo = new Address({
        addressPrimary: false,
        lineOne: 'testStreet'
      })
      const testParty = new Party({ addresses: { primaryOne } })
      testParty.upsertAddress('primaryTwo', primaryTwo)
      expect(testParty.addresses).toEqual({ primaryOne, primaryTwo })
    })
  })

  describe('emails', () => {
    it('should set emails to empty object if class is instantiated without emails', () => {
      const testParty = new Party({})
      expect(testParty.emails).toEqual({})
    })
    it('should throw if Email contains invalid email', () => {
      const testParty = new Party({})
      const testEmails = new Email({ email: 'not an email' })
      function tester() {
        testParty.upsertEmail('test', testEmails)
      }
      expect(tester).toThrowError('Not a valid email')
    })
    it('should set emailPrimary of existing emails to false when supplying new primary email', () => {
      const primaryOne = new Email({
        emailPrimary: true,
        email: 'test@test.com'
      })
      const primaryTwo = new Email({
        emailPrimary: true,
        email: 'test@test.com'
      })
      const primaryThree = new Email({
        emailPrimary: true,
        email: 'test@test.com'
      })
      const testParty = new Party({
        emails: { e1: primaryOne, e2: primaryTwo }
      })
      testParty.upsertEmail('e3', primaryThree)
      expect(testParty.emails.e1.emailPrimary).toBeFalsy()
      expect(testParty.emails.e2.emailPrimary).toBeFalsy()
    })
    it('should throw if attempting to add primary email without primary email', () => {
      const primaryTwo = new Email({
        emailPrimary: false,
        email: 'test@test.com'
      })
      const testParty = new Party({})
      function tester() {
        testParty.upsertEmail('new', primaryTwo)
      }
      expect(tester).toThrowError('At least 1 primary email must be supplied')
    })
    it('should add email and preserve existing ones on upsert', () => {
      const primaryOne = new Email({
        emailPrimary: true,
        email: 'test@test.com'
      })
      const primaryTwo = new Email({
        emailPrimary: false,
        email: 'test@test.com'
      })
      const testParty = new Party({ emails: { primaryOne } })
      testParty.upsertEmail('primaryTwo', primaryTwo)
      expect(testParty.emails).toEqual({ primaryOne, primaryTwo })
    })
  })

    describe('phoneNumbers', () => {
    it('should set phoneNumbers to empty object if class is instantiated without phoneNumbers', () => {
      const testParty = new Party({})
      expect(testParty.phoneNumbers).toEqual({})
    })
    it('should throw if Phone Number contains invalid Phone Number', () => {
      const testParty = new Party({})
      const testPhoneNumbers = new PhoneNumber({ phoneNumber: 'TEST123456' })
      function tester() {
        testParty.upsertPhoneNumber('test', testPhoneNumbers)
      }
      expect(tester).toThrowError('Not a valid Phone number')
    })

  })
})
<commit_msg>Update party.test.js<commit_after>import Party from './party.js'
import { Address, Email, PhoneNumber } from '../Children'
import { Comment, Reference } from '../../children'
import PartyLink from '../../children/Link/PartyLink'

describe('Party', () => {
  describe('serialization', () => {
    it('should serialize properly', () => {
      const test = new Party({ partyId: 'test' })
      expect(JSON.parse(JSON.stringify(test)).partyId).toBeDefined()
      expect(JSON.parse(JSON.stringify(test)).partyId).toEqual('test')
    })
  })
  describe('constructor', () => {
    it('should set url', () => {
      const party = new Party({ url: 'testURL' })
      expect(party.url).toEqual('testURL')
    })

    it('should set displayName', () => {
      const party = new Party({ displayName: 'testDisplayName' })
      expect(party.displayName).toEqual('testDisplayName')
    })

    it('should set legalName', () => {
      const party = new Party({ legalName: 'testLegalName' })
      expect(party.legalName).toEqual('testLegalName')
    })

    it('should throw if attempting to set invalid partyStatus', () => {
      let party
      function construct() {
        party = new Party({ partyStatus: 'notAType' })
      }
      expect(construct).toThrowError(
        new Error('Invalid Party Status: notAType')
      )
    })

    it('should set addresses to empty object if class is instantiated without contacts', () => {
      const testParty = new Party({})
      expect(testParty.addresses).toEqual({})
    })

    it('should set references correctly', () => {
      const party = new Party({})
      party.references = {
        INT: { referenceValue: 'Internal1', referencePrimary: true },
        EXT: new Reference({ referenceValue: 'External1' })
      }
      expect(party.references.INT).toBeDefined()
      expect(party.references.INT).toBeInstanceOf(Reference)
      expect(party.references.INT.referenceValue).toEqual('Internal1')
      expect(party.references.INT.referencePrimary).toBeTruthy()
      expect(party.references.EXT).toBeDefined()
      expect(party.references.EXT.referenceValue).toEqual('External1')
      expect(party.references.EXT.referencePrimary).toBeFalsy()
    })

    it('should throw if setting invalid number of primary references', () => {
      const params = {
        references: {
          noPrimary: { referenceValue: '1' }
        }
      }
      const willThrow = () => {
        const party = new Party(params)
      }
      expect(willThrow).toThrowError(
        'Exactly 1 primary Reference must be supplied - found: 0'
      )
    })

    it('should set comments correctly', () => {
      const party = new Party({})
      party.comments = {
        TRADER: { commentValue: 'Strategy1' },
        BACKOFFICE: new Comment({ commentValue: 'Reconciled' })
      }
      expect(party.comments.TRADER).toBeDefined()
      expect(party.comments.TRADER).toBeInstanceOf(Comment)
      expect(party.comments.TRADER.commentValue).toEqual('Strategy1')
      expect(party.comments.BACKOFFICE).toBeDefined()
      expect(party.comments.BACKOFFICE.commentValue).toEqual('Reconciled')
    })

    it('should set links correctly', () => {
      const party = new Party({})
      party.links = {
        SINGLE1: [{ linkedPartyId: 'ID-S1-1' }],
        SINGLE2: [new PartyLink({ linkedPartyId: 'ID-S2-1' })],
        MULTI1: [
          { linkedPartyId: 'ID-M1-1' },
          new PartyLink({ linkedPartyId: 'ID-M1-2' })
        ]
      }
      expect(party.links.SINGLE1[0]).toBeDefined()
      expect(party.links.SINGLE1[0].linkedPartyId).toEqual('ID-S1-1')
      expect(party.links.SINGLE2[0]).toBeDefined()
      expect(party.links.SINGLE2[0].linkedPartyId).toEqual('ID-S2-1')
      expect(party.links.MULTI1[0]).toBeDefined()
      expect(party.links.MULTI1[0].linkedPartyId).toEqual('ID-M1-1')
      expect(party.links.MULTI1[1]).toBeDefined()
      expect(party.links.MULTI1[1].linkedPartyId).toEqual('ID-M1-2')
    })
  })
  describe('addresses', () => {
    it('should set addressPrimary of existing addresses to false when supplying new primary address', () => {
      const primaryOne = new Address({ addressPrimary: true })
      const primaryTwo = new Address({ addressPrimary: true })
      const primaryThree = new Address({ addressPrimary: true })
      const testParty = new Party({
        addresses: { add1: primaryOne, add2: primaryTwo }
      })
      testParty.upsertAddress('add3', primaryThree)
      expect(testParty.addresses.add1.addressPrimary).toBeFalsy()
      expect(testParty.addresses.add2.addressPrimary).toBeFalsy()
    })
    it('should throw if attempting to add primary address without primary address', () => {
      // const primaryOne = new Address({ addressPrimary: true })
      const primaryTwo = new Address({ addressPrimary: false })
      const testParty = new Party({})
      function tester() {
        testParty.upsertAddress('new', primaryTwo)
      }
      expect(tester).toThrowError('At least 1 primary address must be supplied')
    })
    it('should add address and preserve existing ones on upsert', () => {
      const primaryOne = new Address({
        addressPrimary: true,
        lineOne: 'testRoad'
      })
      const primaryTwo = new Address({
        addressPrimary: false,
        lineOne: 'testStreet'
      })
      const testParty = new Party({ addresses: { primaryOne } })
      testParty.upsertAddress('primaryTwo', primaryTwo)
      expect(testParty.addresses).toEqual({ primaryOne, primaryTwo })
    })
  })

  describe('emails', () => {
    it('should set emails to empty object if class is instantiated without emails', () => {
      const testParty = new Party({})
      expect(testParty.emails).toEqual({})
    })
    it('should throw if Email contains invalid email', () => {
      const testParty = new Party({})
      const testEmails = new Email({ email: 'not an email' })
      function tester() {
        testParty.upsertEmail('test', testEmails)
      }
      expect(tester).toThrowError('Not a valid email')
    })
    it('should set emailPrimary of existing emails to false when supplying new primary email', () => {
      const primaryOne = new Email({
        emailPrimary: true,
        email: 'test@test.com'
      })
      const primaryTwo = new Email({
        emailPrimary: true,
        email: 'test@test.com'
      })
      const primaryThree = new Email({
        emailPrimary: true,
        email: 'test@test.com'
      })
      const testParty = new Party({
        emails: { e1: primaryOne, e2: primaryTwo }
      })
      testParty.upsertEmail('e3', primaryThree)
      expect(testParty.emails.e1.emailPrimary).toBeFalsy()
      expect(testParty.emails.e2.emailPrimary).toBeFalsy()
    })
    it('should throw if attempting to add primary email without primary email', () => {
      const primaryTwo = new Email({
        emailPrimary: false,
        email: 'test@test.com'
      })
      const testParty = new Party({})
      function tester() {
        testParty.upsertEmail('new', primaryTwo)
      }
      expect(tester).toThrowError('At least 1 primary email must be supplied')
    })
    it('should add email and preserve existing ones on upsert', () => {
      const primaryOne = new Email({
        emailPrimary: true,
        email: 'test@test.com'
      })
      const primaryTwo = new Email({
        emailPrimary: false,
        email: 'test@test.com'
      })
      const testParty = new Party({ emails: { primaryOne } })
      testParty.upsertEmail('primaryTwo', primaryTwo)
      expect(testParty.emails).toEqual({ primaryOne, primaryTwo })
    })
  })

    describe('phoneNumbers', () => {
    it('should set phoneNumbers to empty object if class is instantiated without phoneNumbers', () => {
      const testParty = new Party({})
      expect(testParty.phoneNumbers).toEqual({})
    })
    it('should throw if Phone Number contains invalid Phone Number', () => {
      const testParty = new Party({})
      const testPhoneNumbers = new PhoneNumber({ phoneNumber: 'TEST123456', phoneNumberPrimary: true })
      function tester() {
        testParty.upsertPhoneNumber('test', testPhoneNumbers)
      }
      expect(tester).toThrowError('Not a valid Phone number')
    })

  })
})
<|endoftext|>


<commit_before>let { indexOf, binToRaw, Binary } = require('./bintools')
let { assert } = require('./assert')

module.exports = { encoder, decoder }

let STATUS_CODES = {
  '100': 'Continue',
  '101': 'Switching Protocols',
  '102': 'Processing',                 // RFC 2518, obsoleted by RFC 4918
  '200': 'OK',
  '201': 'Created',
  '202': 'Accepted',
  '203': 'Non-Authoritative Information',
  '204': 'No Content',
  '205': 'Reset Content',
  '206': 'Partial Content',
  '207': 'Multi-Status',               // RFC 4918
  '300': 'Multiple Choices',
  '301': 'Moved Permanently',
  '302': 'Moved Temporarily',
  '303': 'See Other',
  '304': 'Not Modified',
  '305': 'Use Proxy',
  '307': 'Temporary Redirect',
  '400': 'Bad Request',
  '401': 'Unauthorized',
  '402': 'Payment Required',
  '403': 'Forbidden',
  '404': 'Not Found',
  '405': 'Method Not Allowed',
  '406': 'Not Acceptable',
  '407': 'Proxy Authentication Required',
  '408': 'Request Time-out',
  '409': 'Conflict',
  '410': 'Gone',
  '411': 'Length Required',
  '412': 'Precondition Failed',
  '413': 'Request Entity Too Large',
  '414': 'Request-URI Too Large',
  '415': 'Unsupported Media Type',
  '416': 'Requested Range Not Satisfiable',
  '417': 'Expectation Failed',
  '418': "I'm a teapot",               // RFC 2324
  '422': 'Unprocessable Entity',       // RFC 4918
  '423': 'Locked',                     // RFC 4918
  '424': 'Failed Dependency',          // RFC 4918
  '425': 'Unordered Collection',       // RFC 4918
  '426': 'Upgrade Required',           // RFC 2817
  '500': 'Internal Server Error',
  '501': 'Not Implemented',
  '502': 'Bad Gateway',
  '503': 'Service Unavailable',
  '504': 'Gateway Time-out',
  '505': 'HTTP Version not supported',
  '506': 'Variant Also Negotiates',    // RFC 2295
  '507': 'Insufficient Storage',       // RFC 4918
  '509': 'Bandwidth Limit Exceeded',
  '510': 'Not Extended'                // RFC 2774
}

function encoder () {
  let mode

  function encodeHead (item) {
    if (!item || item.constructor !== Object) {
      return item
    } else if (typeof item !== 'object') {
      throw new Error(
        'expected an object but got a ' + (typeof item) + ' when encoding data'
      )
    }
    let head, chunkedEncoding
    let version = item.version || 1.1
    if (item.method) {
      let path = item.path
      assert(path && path.length > 0, 'expected non-empty path')
      head = [ item.method + ' ' + item.path + ' HTTP/' + version + '\r\n' ]
    } else {
      let reason = item.reason || STATUS_CODES[item.code]
      head = [ 'HTTP/' + version + ' ' + item.code + ' ' + reason + '\r\n' ]
    }
    let headers = item.headers
    if (Array.isArray(headers)) {
      for (let i = 0, l = headers.length; i < l; i += 2) {
        processHeader(headers[i], headers[i + 1])
      }
    } else {
      for (let key in headers) {
        processHeader(key, headers[key])
      }
    }
    function processHeader (key, value) {
      let lowerKey = key.toLowerCase()
      if (lowerKey === 'transfer-encoding') {
        chunkedEncoding = value.toLowerCase() === 'chunked'
      }
      value = ('' + value).replace(/[\r\n]+/, ' ')
      head[head.length] = key + ': ' + value + '\r\n'
    }

    head[head.length] = '\r\n'

    mode = chunkedEncoding && encodeChunked || encodeRaw
    return head.join('')
  }

  function encodeRaw (item) {
    if (typeof item !== 'string') {
      mode = encodeHead
      return encodeHead(item)
    }
    return item
  }

  function encodeChunked (item) {
    if (typeof item !== 'string') {
      mode = encodeHead
      let extra = encodeHead(item)
      if (extra) {
        return '0\r\n\r\n' + extra
      } else {
        return '0\r\n\r\n'
      }
    }
    if (item.length === 0) {
      mode = encodeHead
    }
    return item.length.toString(16) + '\r\n' + item + '\r\n'
  }

  mode = encodeHead
  function encode (item) {
    return mode(item)
  }
  return encode
}

function decoder () {
  // This decoder is somewhat stateful with 5 different parsing states.
  let mode // state variable that points to various decoders
  let bytesLeft // For counted decoder

  // This state is for decoding the status line and headers.
  function decodeHead (chunk, offset) {
    if (!chunk || chunk.length <= offset) return

    // First make sure we have all the head before continuing
    let index = indexOf(chunk, '\r\n\r\n', offset)
    if (index < 0) {
      if ((chunk.length - offset) < 8 * 1024) return
      // But protect against evil clients by refusing heads over 8K long.
      throw new Error('entity too large')
    }
    // Remember where the head ended and the body started
    let bodyStart = index + 4

    // Parse the status/request line
    let head = {}

    index = indexOf(chunk, '\n', offset) + 1
    let line = binToRaw(chunk, offset, index)
    let match = line.match(/^HTTP\/(\d\.\d) (\d+) ([^\r\n]+)/)
    let version
    if (match) {
      version = match[1]
      head.code = parseInt(match[2], 10)
      head.reason = match[3]
    } else {
      match = line.match(/^([A-Z]+) ([^ ]+) HTTP\/(\d\.\d)/)
      if (match) {
        head.method = match[1]
        head.path = match[2]
        version = match[3]
      } else {
        throw new Error('expected HTTP data')
      }
    }
    head.version = parseFloat(version)
    head.keepAlive = head.version > 1.0

    // We need to inspect some headers to know how to parse the body.
    let contentLength
    let chunkedEncoding

    let headers = head.headers = []
    // Parse the header lines
    let start = index
    while ((index = indexOf(chunk, '\n', index) + 1)) {
      line = binToRaw(chunk, start, index)
      if (line === '\r\n') break
      start = index
      let match = line.match(/^([^:\r\n]+): *([^\r\n]+)/)
      if (!match) {
        throw new Error('Malformed HTTP header: ' + line)
      }
      let key = match[1]
      let value = match[2]
      let lowerKey = key.toLowerCase()

      // Inspect a few headers and remember the values
      if (lowerKey === 'content-length') {
        contentLength = parseInt(value)
      } else if (lowerKey === 'transfer-encoding') {
        chunkedEncoding = value.toLowerCase() === 'chunked'
      } else if (lowerKey === 'connection') {
        head.keepAlive = value.toLowerCase() === 'keep-alive'
      }
      headers.push(key, value)
    }

    if (head.keepAlive
      ? !(chunkedEncoding || (contentLength !== undefined && contentLength > 0))
      : (head.method === 'GET' || head.method === 'HEAD')
    ) {
      mode = decodeEmpty
    } else if (chunkedEncoding) {
      mode = decodeChunked
    } else if (contentLength !== undefined) {
      bytesLeft = contentLength
      mode = decodeCounted
    } else if (!head.keepAlive) {
      mode = decodeRaw
    }
    return [head, bodyStart]
  }

  // This is used for inserting a single empty string into the output string for known empty bodies
  function decodeEmpty (chunk, offset) {
    mode = decodeHead
    return [new Binary(0), offset]
  }

  function decodeRaw (chunk, offset) {
    if (!chunk || chunk.length >= offset) return [new Binary(0)]
    if (chunk.length === 0) return
    return [chunk.slice(offset), chunk.length]
  }

  function decodeChunked (chunk, offset) {
    // Make sure we have at least the length header
    let index = indexOf(chunk, '\r\n', offset)
    if (index < 0) return

    // And parse it
    let hex = binToRaw(chunk, offset, index)
    let length = parseInt(hex, 16)

    // Wait till we have the rest of the body
    let start = hex.length + 2
    let end = start + length
    if ((chunk.length - offset) < end + 2) return

    // An empty chunk means end of stream; reset state.
    if (length === 0) mode = decodeHead

    // Make sure the chunk ends in '\r\n'
    assert(binToRaw(chunk, end, end + 2) === '\r\n', 'Invalid chunk tail')

    return [chunk.slice(start, end), end + 2]
  }

  function decodeCounted (chunk, offset) {
    if (bytesLeft === 0) {
      mode = decodeEmpty
      return mode(chunk)
    }
    let length = chunk.length - offset
    // Make sure we have at least one byte to process
    if (length <= 0) return

    if (length >= bytesLeft) { mode = decodeEmpty }

    // If the entire chunk fits, pass it all through
    if (length <= bytesLeft) {
      bytesLeft -= length
      return [chunk.slice(offset), chunk.length]
    }

    return [chunk.slice(offset, bytesLeft), offset + bytesLeft + 1]
  }

  // Switch between states by changing which decoder mode points to
  mode = decodeHead
  function decode (chunk, offset) {
    return mode(chunk, offset)
  }
  return decode
}
<commit_msg>Create http-codec.js<commit_after>let { indexOf, binToRaw, Binary } = require('./bintools')
let { assert } = require('./assert')

module.exports = { encoder, decoder }

let STATUS_CODES = {
  '100': 'Continue',
  '101': 'Switching Protocols',
  '102': 'Processing',                 // RFC 2518, obsoleted by RFC 4918
  '200': 'OK',
  '201': 'Created',
  '202': 'Accepted',
  '203': 'Non-Authoritative Information',
  '204': 'No Content',
  '205': 'Reset Content',
  '206': 'Partial Content',
  '207': 'Multi-Status',               // RFC 4918
  '300': 'Multiple Choices',
  '301': 'Moved Permanently',
  '302': 'Moved Temporarily',
  '303': 'See Other',
  '304': 'Not Modified',
  '305': 'Use Proxy',
  '307': 'Temporary Redirect',
  '400': 'Bad Request',
  '401': 'Unauthorized',
  '402': 'Payment Required',
  '403': 'Forbidden',
  '404': 'Not Found',
  '405': 'Method Not Allowed',
  '406': 'Not Acceptable',
  '407': 'Proxy Authentication Required',
  '408': 'Request Time-out',
  '409': 'Conflict',
  '410': 'Gone',
  '411': 'Length Required',
  '412': 'Precondition Failed',
  '413': 'Request Entity Too Large',
  '414': 'Request-URI Too Large',
  '415': 'Unsupported Media Type',
  '416': 'Requested Range Not Satisfiable',
  '417': 'Expectation Failed',
  '418': "I'm a teapot",               // RFC 2324
  '422': 'Unprocessable Entity',       // RFC 4918
  '423': 'Locked',                     // RFC 4918
  '424': 'Failed Dependency',          // RFC 4918
  '425': 'Unordered Collection',       // RFC 4918
  '426': 'Upgrade Required',           // RFC 2817
  '500': 'Internal Server Error',
  '501': 'Not Implemented',
  '502': 'Bad Gateway',
  '503': 'Service Unavailable',
  '504': 'Gateway Time-out',
  '505': 'HTTP Version not supported',
  '506': 'Variant Also Negotiates',    // RFC 2295
  '507': 'Insufficient Storage',       // RFC 4918
  '509': 'Bandwidth Limit Exceeded',
  '510': 'Not Extended'                // RFC 2774
}

function encoder () {
  let mode

  function encodeHead (item) {
    if (!item || item.constructor !== Object) {
      return item
    } else if (typeof item !== 'object') {
      throw new Error(
        'expected an object but got a ' + (typeof item) + ' when encoding data'
      )
    }
    let head, chunkedEncoding
    let version = item.version || 1.1
    if (item.method) {
      let path = item.path
      assert(path && path.length > 0, 'expected non-empty path')
      head = [ item.method + ' ' + item.path + ' HTTP/' + version + '\r\n' ]
    } else {
      let reason = item.reason || STATUS_CODES[item.code]
      head = [ 'HTTP/' + version + ' ' + item.code + ' ' + reason + '\r\n' ]
    }
    let headers = item.headers
    if (Array.isArray(headers)) {
      for (let i = 0, l = headers.length; i < l; i += 2) {
        processHeader(headers[i], headers[i + 1])
      }
    } else {
      for (let key in headers) {
        processHeader(key, headers[key])
      }
    }
    function processHeader (key, value) {
      let lowerKey = key.toLowerCase()
      if (lowerKey === 'transfer-encoding') {
        chunkedEncoding = value.toLowerCase() === 'chunked'
      }
      value = ('' + value).replace(/[\r\n]+/, ' ')
      head[head.length] = key + ': ' + value + '\r\n'
    }

    head[head.length] = '\r\n'

    mode = chunkedEncoding && encodeChunked || encodeRaw
    return head.join('')
  }

  function encodeRaw (item) {
    if (typeof item !== 'string') {
      mode = encodeHead
      return encodeHead(item)
    }
    return item
  }

  function encodeChunked (item) {
    if (typeof item !== 'string') {
      mode = encodeHead
      let extra = encodeHead(item)
      if (extra) {
        return '0\r\n\r\n' + extra
      } else {
        return '0\r\n\r\n'
      }
    }
    if (item.length === 0) {
      mode = encodeHead
    }
    return item.length.toString(16) + '\r\n' + item + '\r\n'
  }

  mode = encodeHead
  function encode (item) {
    return mode(item)
  }
  return encode
}

function decoder () {
  // This decoder is somewhat stateful with 5 different parsing states.
  let mode // state variable that points to various decoders
  let bytesLeft // For counted decoder

  // This state is for decoding the status line and headers.
  function decodeHead (chunk, offset) {
    if (!chunk || chunk.length <= offset) return

    // First make sure we have all the head before continuing
    let index = indexOf(chunk, '\r\n\r\n', offset)
    if (index < 0) {
      if ((chunk.length - offset) < 8 * 1024) return
      // But protect against evil clients by refusing heads over 8K long.
      throw new Error('entity too large')
    }
    // Remember where the head ended and the body started
    let bodyStart = index + 4

    // Parse the status/request line
    let head = {}

    index = indexOf(chunk, '\n', offset) + 1
    let line = binToRaw(chunk, offset, index)
    let match = line.match(/^HTTP\/(\d\.\d) (\d+) ([^\r\n]+)/)
    let version
    if (match) {
      version = match[1]
      head.code = parseInt(match[2], 10)
      head.reason = match[3]
    } else {
      match = line.match(/^([A-Z]+) ([^ ]+) HTTP\/(\d\.\d)/)
      if (match) {
        head.method = match[1]
        head.path = match[2]
        version = match[3]
      } else {
        throw new Error('expected HTTP data')
      }
    }
    head.version = parseFloat(version)
    head.keepAlive = head.version > 1.0

    // We need to inspect some headers to know how to parse the body.
    let contentLength
    let chunkedEncoding

    let headers = head.headers = []
    // Parse the header lines
    let start = index
    while ((index = indexOf(chunk, '\n', index) + 1)) {
      line = binToRaw(chunk, start, index)
      if (line === '\r\n') break
      start = index
      let match = line.match(/^([^:\r\n]+): *([^\r\n]+)/)
      if (!match) {
        throw new Error('Malformed HTTP header: ' + line)
      }
      let key = match[1]
      let value = match[2]
      let lowerKey = key.toLowerCase()

      // Inspect a few headers and remember the values
      if (lowerKey === 'content-length') {
        contentLength = parseInt(value)
      } else if (lowerKey === 'transfer-encoding') {
        chunkedEncoding = value.toLowerCase() === 'chunked'
      } else if (lowerKey === 'connection') {
        head.keepAlive = value.toLowerCase() === 'keep-alive'
      }
      headers.push(key, value)
    }

    if (head.keepAlive
      ? !(chunkedEncoding || (contentLength !== undefined && contentLength > 0))
      : (head.method === 'GET' || head.method === 'HEAD')
    ) {
      mode = decodeEmpty
    } else if (chunkedEncoding) {
      mode = decodeChunked
    } else if (contentLength !== undefined) {
      bytesLeft = contentLength
      mode = decodeCounted
    } else if (!head.keepAlive) {
      mode = decodeRaw
    }
    return [head, bodyStart]
  }

  // This is used for inserting a single empty string into the output string for known empty bodies
  function decodeEmpty (chunk, offset) {
    mode = decodeHead
    return [new Binary(0), offset]
  }

  function decodeRaw (chunk, offset) {
    if (!chunk || chunk.length >= offset) return [new Binary(0)]
    if (chunk.length === 0) return
    return [chunk.slice(offset), chunk.length]
  }

  function decodeChunked (chunk, offset) {
    // Make sure we have at least the length header
    let index = indexOf(chunk, '\r\n', offset)
    if (index < 0) return

    // And parse it
    let hex = binToRaw(chunk, offset, index)
    let length = parseInt(hex, 16)

    // Wait till we have the rest of the body
    let start = hex.length + 2
    let end = start + length
    if ((chunk.length - offset) < end + 2) return

    // An empty chunk means end of stream; reset state.
    if (length === 0) mode = decodeHead

    // Make sure the chunk ends in '\r\n'
    assert(binToRaw(chunk, end, end + 2) === '\r\n', 'Invalid chunk tail')

    return [chunk.slice(start, end), end + 2]
  }

  function decodeCounted (chunk, offset) {
    if (bytesLeft === 0) {
      mode = decodeEmpty
      return mode(chunk, offset)
    }
    let length = chunk.length - offset
    // Make sure we have at least one byte to process
    if (length <= 0) return

    if (length >= bytesLeft) { mode = decodeEmpty }

    // If the entire chunk fits, pass it all through
    if (length <= bytesLeft) {
      bytesLeft -= length
      return [chunk.slice(offset), chunk.length]
    }

    return [chunk.slice(offset, bytesLeft), offset + bytesLeft + 1]
  }

  // Switch between states by changing which decoder mode points to
  mode = decodeHead
  function decode (chunk, offset) {
    return mode(chunk, offset)
  }
  return decode
}
<|endoftext|>


<commit_before>/**
 * Created by mike on 01/12/16.
 */
let DatabaseOperation = require('./database.js').DatabaseOperation
let http = require('http')
let https = require('https')
let fs = require('fs')
let crypto = require('crypto')
let temp = require('os').tmpdir()
let path = require('path')
let u = require('url')

class URLHandler {

    static downloadFile(url, callback) {
        UrlHelper.download(url, function (fname) {
            let file = []
            if (fname != ''){
                callback && callback(null, fname)
            } else {
                callback && callback('wrong_url', [])
            }
        })
    }

    static isFileInDb(url, callback){
        DatabaseOperation.File.GetAllFiles(null, null, null, function comp (err, rows){
            download(url, function (fname){
                if (fname === '') {
                    callback&&callback('err', null)
                } else {
                    UrlHelper.createChecksum(fname, function (checksum) {
                        let r = rows.filter(UrlHelper.isChecksumInArr, checksum)
                        if (r.length > 1) {
                            callback && callback(null, true)
                        } else {
                            callback && callback(null, false)
                        }
                    })
                }
            })
        })
    }
}

exports.URLHandler = URLHandler

class UrlHelper {

    static createChecksum (fpath, callback) {
        //console.log ('Tworze checksum')
        fpath = fpath.toString()
        let checksum = crypto.createHash('md5')
        let rs = fs.createReadStream(fpath)
        fs.readFile(fpath, function hashFile(err, data) {
            checksum.update(data, 'utf8')
            callback && callback(checksum.digest('hex'))
        })
    }

    static isChecksumInArr(element){
        return (element.Checksum === this)
    }

    static download(url, callback) {
        let parts = url.split('/')
        let parts2 = parts[parts.length - 1].split('.')
        //console.log(temp)
        let fname = path.join(temp, parts2[0] + '.pdf')
        //let fname =  temp + '/'  + parts2[0] + '.pdf'
        //fname = fname.toString()
        let pdf = fs.createWriteStream(fname)

        if (url.substring(0,5) ==='https') {
            //console.log('pobieram')
            let req = https.get(url, function(response) {
                response.pipe(pdf)
                response.on('error', function() {
                    //console.log('blad przy pobieraniu')
                    callback && callback('')
                })
                response.on('end', function() {
                    //console.log('pobrano')
                    if (response.statusCode === 200) {
                        callback && callback(fname)
                    } else {
                        callback && callback('')
                    }
                })
            })
            req.on('error', (err)=> {
                callback && callback('')
            })
            req.end()
        } else if (url.substring(0,4) ==='http') {
            //console.log('pobieram')
            let req = http.get(url, function(response) {
                //console.log(response.statusCode)
                response.on('error', function() {
                    //console.log('blad przy pobieraniu')
                    callback && callback('')
                })
                response.on('end', function() {
                    //console.log('pobrano')
                    if (response.statusCode === 200) {
                        callback && callback(fname)
                    } else {
                        callback && callback('')
                    }
                })
            })
            req.on('error', (err)=> {
                callback && callback('')
            })
            req.end()
        } else {
            callback && callback('')
        }
    }
}<commit_msg>dl fix<commit_after>/**
 * Created by mike on 01/12/16.
 */
let DatabaseOperation = require('./database.js').DatabaseOperation
let http = require('http')
let https = require('https')
let fs = require('fs')
let crypto = require('crypto')
let temp = require('os').tmpdir()
let path = require('path')
let u = require('url')

class URLHandler {

    static downloadFile(url, callback) {
        UrlHelper.download(url, function (fname) {
            let file = []
            if (fname != ''){
                callback && callback(null, fname)
            } else {
                callback && callback('wrong_url', [])
            }
        })
    }

    static isFileInDb(url, callback){
        DatabaseOperation.File.GetAllFiles(null, null, null, function comp (err, rows){
            download(url, function (fname){
                if (fname === '') {
                    callback&&callback('err', null)
                } else {
                    UrlHelper.createChecksum(fname, function (checksum) {
                        let r = rows.filter(UrlHelper.isChecksumInArr, checksum)
                        if (r.length > 1) {
                            callback && callback(null, true)
                        } else {
                            callback && callback(null, false)
                        }
                    })
                }
            })
        })
    }
}

exports.URLHandler = URLHandler

class UrlHelper {

    static createChecksum (fpath, callback) {
        //console.log ('Tworze checksum')
        fpath = fpath.toString()
        let checksum = crypto.createHash('md5')
        let rs = fs.createReadStream(fpath)
        fs.readFile(fpath, function hashFile(err, data) {
            checksum.update(data, 'utf8')
            callback && callback(checksum.digest('hex'))
        })
    }

    static isChecksumInArr(element){
        return (element.Checksum === this)
    }

    static download(url, callback) {
        let parts = url.split('/')
        let parts2 = parts[parts.length - 1].split('.')
        //console.log(temp)
        let fname = path.join(temp, parts2[0] + '.pdf')
        //let fname =  temp + '/'  + parts2[0] + '.pdf'
        //fname = fname.toString()
        let pdf = fs.createWriteStream(fname)

        if (url.substring(0,5) ==='https') {
            //console.log('pobieram')
            let req = https.get(url, function(response) {
                response.pipe(pdf)
                response.on('error', function() {
                    //console.log('blad przy pobieraniu')
                    callback && callback('')
                })
                response.on('end', function() {
                    //console.log('pobrano')
                    if (response.statusCode === 200) {
                        callback && callback(fname)
                    } else {
                        callback && callback('')
                    }
                })
            })
            req.on('error', (err)=> {
                callback && callback('')
            })
            req.end()
        } else if (url.substring(0,4) ==='http') {
            //console.log('pobieram')
            let req = http.get(url, function(response) {
                response.pipe(pdf)
                //console.log(response.statusCode)
                response.on('error', function() {
                    //console.log('blad przy pobieraniu')
                    callback && callback('')
                })
                response.on('end', function() {
                    //console.log('pobrano')
                    if (response.statusCode === 200) {
                        callback && callback(fname)
                    } else {
                        callback && callback('')
                    }
                })
            })
            req.on('error', (err)=> {
                callback && callback('')
            })
            req.end()
        } else {
            callback && callback('')
        }
    }
}<|endoftext|>


<commit_before>function validateNumberOnly(self) {
  self = $(self);
  var val = parseInt(self.val().replace(/[^0-9]/g, "") || 10);
  var max = parseInt(self.attr('max') || 999999);
  var min = parseInt(self.attr('min') ||-999999);
  self.val(Math.max(min, Math.min(max, val)));
}

function parseMatrix(orig) {
  var s = $.trim(orig);
  var rows = s.split(/[\r\n]+/);
  rows = $.map(rows, function(r) {
    return $.trim(r).split(/\s+/).join(" ");
  });
  return rows.join("  ");
}

function showNextHint(elm) {
  var hidden = $(elm).siblings(":hidden");
  hidden.first().animate(CONST.showAnimation);
  if(hidden.length === 1) $(elm).animate(CONST.hideAnimation);
}

function renderStarred(question) {
  if(!window.loggedIn) return '';
  c = "";
  c += '
';

  c += question.starred ? '★ ' : '';
  c += 'Frage ';
  c += question.starred ? 'gemerkt' : 'merken';
  c += '
';

  return c;
}

function handleStarredClick(link) {
  var l = $(link);
  var s = l.data('starred');
  var id = l.data('id');
  var r = s ? Routes.unstar_question_path(id) : Routes.star_question_path(id);

  $.ajax({
    url: r,
  }).done(function(data) {
    var fakeQ = {id: id, starred: data};
    l.parent().replaceWith(renderStarred(fakeQ));
  }).fail(function() {
    l.text('irgendwas ist kaputt…').addClass('disable');
  });
}

function getQuestionById(id) {
  var quest = null;
  $.each(window.currentHitme.questions, function(ind, q) {
    if(q.id === id) {
      quest = q;
      return false;
    }
  });
  return quest;
}

function storeStats(quest_id, answ_id) {
  $.post(Routes.new_stat_path(quest_id, answ_id), {a: "b"});
}

function ensureValidDifficultySelection() {
  var any = false;
  $('input[type=checkbox][name^=difficulty]').each(function() {
    if($(this).is('checked')) {
      any = true;
      return false;
    }
  });
  if(!any) $('input[type=checkbox][name^=difficulty]:first').prop('checked', true);
}

function disableOptions()  {
  $('#options input').attr("disabled", "disabled");
  $('#options').animate(CONST.hideAnimation);
}

function enableOptions() {
  $('#options input').removeAttr("disabled");
}

function hideCategories() {
  $('h3 + .toggle:visible').each(function(i, cat) {
    $(cat).prev().click();
  });
  var s = $('#categories a.button, #start-button');
  s.addClass('disable').attr('oldonclick', s.data('onclick'));
}

function getDifficulties() {
  var diff = [];
  $('input[type=checkbox][name^=difficulty]').each(function() {
    if(this.checked) {
      diff.push($(this).val());
    }
  });
  return diff.join("_");
}

function showAllCategories() {
  $('#categories .toggle').animate(CONST.showAnimation);
  var s = $('a.disable, #start-button');
  s.removeClass('disable').attr('onclick', s.data('oldonclick'));
}

function animateVisibilityHiddenShow(elms) {
  elms.css('visibility','visible').hide().fadeIn('slow');
}

function getURLForRootQuestions(categoryIds) {
  var diff = this.getDifficulties();
  var count = $('#quantity').val();
  var studyPath = $('#study_path').val();

  var h = {categories: categoryIds.join("_"), count: count, difficulty: diff, study_path: studyPath}

  return Routes.main_question_path(h);
}

function getRootQuestions(categoryIds, context, successCallback) {
  // example: http://0.0.0.0:3000/main/questions?categories=6_8&count=10&difficulty=10_20_30&study_path=3
  $.ajax({
    url: getURLForRootQuestions(categoryIds),
  }).done(function(data) {
    successCallback(context, data);
  }).fail(function() {
    alert("Die Anfrage konnte leider nicht bearbeitet werden. Möglicherweise hat der Server ein Problem.");
  });
}

function answersGivenCount() {
  var a = window.H.answersGiven;
  return a.fail.length + a.correct.length + a.skip.length;
}

function maybeInsertSubquestion(aid) {
  if(!$('#subquestions').is(':checked')) {
    // user doesn’t want subquestions, skip
    return;
  }

  // only try to show subquestion half of the time
  //~ if(Math.random() < 0.5) return; // fix as per #5
  var q = window.currentQuestion;
  var s;
  $.each(q.answers, function(ind, answ) {
    if(answ.id === parseInt(aid)) {
      s = answ.subquestion;
      return false;
    }
  });
  // this answer doesn’t have a subquestion
  if(!s) return;
  var c = window.currentHitme;
  var p = c.questPositionPointer;
  c.questions.splice(p+1, 0, s);
}

H = {};
window.H = H;

window.currentHitme = null;

Array.prototype.diff = function(a) {
    return this.filter(function(i) {return !(a.indexOf(i) > -1);});
};

window.CONST = {
  hideAnimation: {opacity: 'hide', height: 'hide', marginTop: 'hide',
    marginBottom: 'hide', paddingTop: 'hide', paddingBottom: 'hide'},
  showAnimation: {opacity: 'show', height: 'show', marginTop: 'show',
    marginBottom: 'show', paddingTop: 'show', paddingBottom: 'show'},
  stayAtBottom: { duration: 'slow',
    step: function(now, fx) {
      $('html, body').scrollTop(99999999);
    }
  }
}

// constructor
H.Hitme = function() {
  this.cats = $('.inline-chooser .active').map(function(i, m) { return $(m).data("id"); }).get();
  if(this.cats.length === 0) {
    alert("Wähle bitte eine Kategorie aus.");
    return;
  }

  this._this = this;
  disableOptions();
  ensureValidDifficultySelection();
  hideCategories();
  getRootQuestions(this.cats, this, this.rootQuestionsAvailable);
  this.questPositionPointer = -1;
  this.answersGiven = {correct: [], fail: [], skip: []};
};

// members
H.Hitme.prototype = {
  giveMore: function() {
    $('.hideMeOnMore').remove();
    H.hitme(this.cat);
  },

  rootQuestionsAvailable: function(_this, data) {
    _this.questions = data;
    _this.showNext();
  },

  _renderAnswersForQuestion: function(quest) {
    var s = "";
    $.each(quest.answers, function(ind, a) {
      s += '
'
      s += ''+a.html+'';
      s += ''+a.correctness+'';
      s += 'Deine Antwort';
      s += '
';
    });
    return s;
  },

  _handleAnswerClick: function() {
    var answ = $(this);
    var linkBox = answ.parents('.answer-chooser, .answer-chooser-matrix').first();
    var boxSelector = '#blockQ' + answ.data('qid');
    var box = $(boxSelector);

    var isMatrix = getQuestionById(answ.data('qid')).matrix;
    if(isMatrix && answ.data('aid') === 1) {
      var txt = box.find("textarea");
      var m = parseMatrix(txt.val());
      txt.attr('disabled', 'disabled');
      var isCorr = window.currentQuestion.matrix_solution === m;
      answ.data('correct', isCorr);
      if(!isCorr) answ.data('aid', 0);
    }

    storeStats(answ.data('qid'), answ.data('aid'));

    var c = answ.data('correct');
    switch(c) {
      case "true":
      case true:
      window.currentHitme.answersGiven.correct.push(boxSelector);
      box.addClass('reveal');
      maybeInsertSubquestion(answ.data('aid'));
      break;

      case "false":
      case false:
      window.currentHitme.answersGiven.fail.push(boxSelector);
      box.addClass('reveal');
      maybeInsertSubquestion(answ.data('aid'));
      break;

      default:
      window.currentHitme.answersGiven.skip.push(boxSelector);
    }

    linkBox.find('a').addClass('disable');
    animateVisibilityHiddenShow(answ.siblings().last());
    window.currentHitme.showNext();
  },

  _showNextQuestion: function() {
    this.questPositionPointer++;
    var q = window.currentQuestion = this.questions[this.questPositionPointer];

    var code = '
'
      + q.html
      + '
';

    if(q.hints.length >= 1) {
      code += '
'
      $.each(q.hints, function(ind, hint) {
        code += '
'+hint+'
';
      });
      code += 'Hinweis anzeigen';
      code += '
';
    }

    code += renderStarred(q);

    var cls;

    if(q.matrix) {
      cls = 'answer-chooser-matrix';

      code += 'Trage unten die Lösung ein. Matrizen schreibst Du einfach mittels Leerzeichen und Zeilenumbrüchen. Die Anzahl der Leerzeichen ist dabei egal.

'
      var a = q.answers[0];
      code += '
';
      code += 'Deine Lösung';
      code += '<textarea id="a'+a.id+'" class="matrixmode"></textarea>
';
      code += '
';
      code += '
';
      code += 'Unsere Lösung
';
      code += a.html+'
';
      code += '
';
      code += '
';
      code += 'Antwort übernehmen';
      code += 'Frage überspringen';
      code += '
';
    } else {
      cls = 'answer-chooser';
      code += '
'
        + this._renderAnswersForQuestion(q)
        +'
Frage überspringenDu hast diese Frage übersprungen
'
        + '
';
    }

    code += '
';

    $(code).appendTo('body').animate(CONST.showAnimation, CONST.stayAtBottom);
    $('.'+cls+':last').one('click', 'a', this._handleAnswerClick);
  },

  _showFinishDialog: function() {
    var sum = this.answersGiven.correct.length + this.answersGiven.fail.length;

    var code = '
'
      + '
Fertig!
'
      + '
Du hast den aktuellen Block abgeschlossen. Insgesamt hast Du '+sum+' Fragen beantwortet und davon '+this.answersGiven.fail.length+' falsch. Scrolle nach oben um jeweils die Antworten für die Fragen zu sehen.
'
      + '
'
      + 'Gib mir nochmal '+$('#quantity').val()+'!'
      + 'Einstellungen ändern'
      + '
';

    if($('#comiccheckbox').is(':checked')) {
      code = code
        + '

Zur Belohnung ist hier ein zufälliges XKCD-Comic:
'
        + '

'
        + '
(eigentlich müsste hier ein Link auf XKCD stehen – zu Deinem Schutz fehlt er aber)'
        + '
';
    }

    $(code).appendTo('body').animate(CONST.showAnimation, CONST.stayAtBottom);

    animateVisibilityHiddenShow($('.reveal .answer-chooser > div > span:nth-child(2), .reveal .initiallyHidden'));

    var allCorr = this.answersGiven.correct.diff(this.answersGiven.fail).join(',');
    var anyFail = this.answersGiven.fail.join(',');
    $(allCorr).addClass('correct');
    $(anyFail).addClass('wrong');

    $('.xkcd:last').load(Routes.random_xkcd_path() + ' #comic');
  },

  showNext: function() {
    if(this.questions.length === 0) {
      alert("Keine Fragen für die aktuellen Einstellungen gefunden.\n\nVersuche es mit anderen Einstellungen nochmal.");
      enableOptions();
      showAllCategories();
    // no more available questions?
    } else if(this.questions.length-1 === this.questPositionPointer) {
      this._showFinishDialog();
    // reached user defined question limit
    } else if(answersGivenCount >= parseInt($('#quantity').val())) {
      this._showFinishDialog();
    } else {
      this._showNextQuestion();
    }
  },

  skipCurrentQuestion: function(self) {
    $(self).animate(CONST.hideAnimation);
    this.showNext();
  }
}

// convenience generator
H.hitme = function (categoryElement) {
  var h = new H.Hitme(categoryElement);
  window.currentHitme = h;
  return h;
}
<commit_msg>implement re-show for skipped questions (fixes #14)<commit_after>function validateNumberOnly(self) {
  self = $(self);
  var val = parseInt(self.val().replace(/[^0-9]/g, "") || 10);
  var max = parseInt(self.attr('max') || 999999);
  var min = parseInt(self.attr('min') ||-999999);
  self.val(Math.max(min, Math.min(max, val)));
}

function parseMatrix(orig) {
  var s = $.trim(orig);
  var rows = s.split(/[\r\n]+/);
  rows = $.map(rows, function(r) {
    return $.trim(r).split(/\s+/).join(" ");
  });
  return rows.join("  ");
}

function showNextHint(elm) {
  var hidden = $(elm).siblings(":hidden");
  hidden.first().animate(CONST.showAnimation);
  if(hidden.length === 1) $(elm).animate(CONST.hideAnimation);
}

function renderStarred(question) {
  if(!window.loggedIn) return '';
  c = "";
  c += '
';

  c += question.starred ? '★ ' : '';
  c += 'Frage ';
  c += question.starred ? 'gemerkt' : 'merken';
  c += '
';

  return c;
}

function handleStarredClick(link) {
  var l = $(link);
  var s = l.data('starred');
  var id = l.data('id');
  var r = s ? Routes.unstar_question_path(id) : Routes.star_question_path(id);

  $.ajax({
    url: r,
  }).done(function(data) {
    var fakeQ = {id: id, starred: data};
    l.parent().replaceWith(renderStarred(fakeQ));
  }).fail(function() {
    l.text('irgendwas ist kaputt…').addClass('disable');
  });
}

function getQuestionById(id) {
  var quest = null;
  $.each(window.currentHitme.questions, function(ind, q) {
    if(q.id === id) {
      quest = q;
      return false;
    }
  });
  return quest;
}

function storeStats(quest_id, answ_id) {
  $.post(Routes.new_stat_path(quest_id, answ_id), {a: "b"});
}

function ensureValidDifficultySelection() {
  var any = false;
  $('input[type=checkbox][name^=difficulty]').each(function() {
    if($(this).is('checked')) {
      any = true;
      return false;
    }
  });
  if(!any) $('input[type=checkbox][name^=difficulty]:first').prop('checked', true);
}

function disableOptions()  {
  $('#options input').attr("disabled", "disabled");
  $('#options').animate(CONST.hideAnimation);
}

function enableOptions() {
  $('#options input').removeAttr("disabled");
}

function hideCategories() {
  $('h3 + .toggle:visible').each(function(i, cat) {
    $(cat).prev().click();
  });
  var s = $('#categories a.button, #start-button');
  disableLinks(s);
}

function getDifficulties() {
  var diff = [];
  $('input[type=checkbox][name^=difficulty]').each(function() {
    if(this.checked) {
      diff.push($(this).val());
    }
  });
  return diff.join("_");
}

function showAllCategories() {
  $('#categories .toggle').animate(CONST.showAnimation);
  var s = $('a.disable, #start-button');
  enableLinks(s);
}

function animateVisibilityHiddenShow(elms) {
  elms.css('visibility','visible').hide().fadeIn('slow');
}

function disableLinks(selector) {
  $(selector).each(function(ind, s) {
    s = $(s);
    s.addClass('disable')
      .data('oldonclick', s.attr('onclick'))
      .attr('onclick', '');
  });
}

function enableLinks(selector) {
  $(selector).each(function(ind, s) {
    s = $(s);
    s.removeClass('disable').attr('onclick', s.data('oldonclick'));
  });
}

function getURLForRootQuestions(categoryIds) {
  var diff = this.getDifficulties();
  var count = $('#quantity').val();
  var studyPath = $('#study_path').val();

  var h = {categories: categoryIds.join("_"), count: count, difficulty: diff, study_path: studyPath}

  return Routes.main_question_path(h);
}

function getRootQuestions(categoryIds, context, successCallback) {
  // example: http://0.0.0.0:3000/main/questions?categories=6_8&count=10&difficulty=10_20_30&study_path=3
  $.ajax({
    url: getURLForRootQuestions(categoryIds),
  }).done(function(data) {
    successCallback(context, data);
  }).fail(function() {
    alert("Die Anfrage konnte leider nicht bearbeitet werden. Möglicherweise hat der Server ein Problem.");
  });
}

function answersGivenCount() {
  var a = window.H.answersGiven;
  return a.fail.length + a.correct.length + a.skip.length;
}

function maybeInsertSubquestion(aid) {
  if(!$('#subquestions').is(':checked')) {
    // user doesn’t want subquestions, skip
    return;
  }

  // only try to show subquestion half of the time
  //~ if(Math.random() < 0.5) return; // fix as per #5
  var q = window.currentQuestion;
  var s;
  $.each(q.answers, function(ind, answ) {
    if(answ.id === parseInt(aid)) {
      s = answ.subquestion;
      return false;
    }
  });
  // this answer doesn’t have a subquestion
  if(!s) return;
  var c = window.currentHitme;
  var p = c.questPositionPointer;
  c.questions.splice(p+1, 0, s);
}

H = {};
window.H = H;

window.currentHitme = null;

Array.prototype.diff = function(a) {
    return this.filter(function(i) {return !(a.indexOf(i) > -1);});
};

window.CONST = {
  hideAnimation: {opacity: 'hide', height: 'hide', marginTop: 'hide',
    marginBottom: 'hide', paddingTop: 'hide', paddingBottom: 'hide'},
  showAnimation: {opacity: 'show', height: 'show', marginTop: 'show',
    marginBottom: 'show', paddingTop: 'show', paddingBottom: 'show'},
  stayAtBottom: { duration: 'slow',
    step: function(now, fx) {
      $('html, body').scrollTop(99999999);
    }
  }
}

// constructor
H.Hitme = function() {
  this.cats = $('.inline-chooser .active').map(function(i, m) { return $(m).data("id"); }).get();
  if(this.cats.length === 0) {
    alert("Wähle bitte eine Kategorie aus.");
    return;
  }

  this._this = this;
  disableOptions();
  ensureValidDifficultySelection();
  hideCategories();
  getRootQuestions(this.cats, this, this.rootQuestionsAvailable);
  this.questPositionPointer = -1;
  this.nagAboutSkippedQuestions = true;
  this.skippedQuestionsData = [];
  this.answersGiven = {correct: [], fail: [], skip: []};
};

// members
H.Hitme.prototype = {
  giveMore: function() {
    $('.hideMeOnMore').remove();
    H.hitme(this.cat);
  },

  rootQuestionsAvailable: function(_this, data) {
    _this.questions = data;
    _this.showNext();
  },

  _renderAnswersForQuestion: function(quest) {
    var s = "";
    $.each(quest.answers, function(ind, a) {
      s += '
'
      s += ''+a.html+'';
      s += ''+a.correctness+'';
      s += 'Deine Antwort';
      s += '
';
    });
    return s;
  },

  _handleAnswerClick: function() {
    var answ = $(this);
    var linkBox = answ.parents('.answer-chooser, .answer-chooser-matrix').first();
    var boxSelector = '#blockQ' + answ.data('qid');
    // i.e. skipped questions are shown again
    if(!this.nagAboutSkippedQuestions) boxSelector += 'repeat';
    var box = $(boxSelector);

    var isMatrix = getQuestionById(answ.data('qid')).matrix;
    if(isMatrix && answ.data('aid') === 1) {
      var txt = box.find("textarea");
      var m = parseMatrix(txt.val());
      txt.attr('disabled', 'disabled');
      var isCorr = window.currentQuestion.matrix_solution === m;
      answ.data('correct', isCorr);
      if(!isCorr) answ.data('aid', 0);
    }

    storeStats(answ.data('qid'), answ.data('aid'));

    var c = answ.data('correct');
    switch(c) {
      case "true":
      case true:
      window.currentHitme.answersGiven.correct.push(boxSelector);
      box.addClass('reveal');
      maybeInsertSubquestion(answ.data('aid'));
      break;

      case "false":
      case false:
      window.currentHitme.answersGiven.fail.push(boxSelector);
      box.addClass('reveal');
      maybeInsertSubquestion(answ.data('aid'));
      break;

      default:
      window.currentHitme.skippedQuestionsData.push(window.currentQuestion);
      window.currentHitme.answersGiven.skip.push(boxSelector);
    }

    linkBox.find('a').addClass('disable');
    animateVisibilityHiddenShow(answ.siblings().last());
    window.currentHitme.showNext();
  },

  _showNextQuestion: function() {
    this.questPositionPointer++;
    var q = window.currentQuestion = this.questions[this.questPositionPointer];

    var blockId = 'blockQ' + q.id;
    // i.e. skipped questions are shown again
    if(!this.nagAboutSkippedQuestions) blockId += 'repeat';

    var code = '
'
      + q.html
      + '
';

    if(q.hints.length >= 1) {
      code += '
'
      $.each(q.hints, function(ind, hint) {
        code += '
'+hint+'
';
      });
      code += 'Hinweis anzeigen';
      code += '
';
    }

    code += renderStarred(q);

    var cls;

    if(q.matrix) {
      cls = 'answer-chooser-matrix';

      code += 'Trage unten die Lösung ein. Matrizen schreibst Du einfach mittels Leerzeichen und Zeilenumbrüchen. Die Anzahl der Leerzeichen ist dabei egal.

'
      var a = q.answers[0];
      code += '
';
      code += 'Deine Lösung';
      code += '<textarea id="a'+a.id+'" class="matrixmode"></textarea>
';
      code += '
';
      code += '
';
      code += 'Unsere Lösung
';
      code += a.html+'
';
      code += '
';
      code += '
';
      code += 'Antwort übernehmen';
      code += 'Frage überspringen';
      code += '
';
    } else {
      cls = 'answer-chooser';
      code += '
'
        + this._renderAnswersForQuestion(q)
        +'
Frage überspringenDu hast diese Frage übersprungen
'
        + '
';
    }

    code += '
';

    $(code).appendTo('body').animate(CONST.showAnimation, CONST.stayAtBottom);
    $('.'+cls+':last').one('click', 'a', this._handleAnswerClick);
  },

  _reshowSkippedQuestions: function() {
    var w = window.currentHitme;
    w.answersGiven.skip = [];
    w.questions = $.extend(true, [], w.skippedQuestionsData); // deep copy
    w.questPositionPointer = -1;
    w.showNext();
  },

  _showSkippedQuestionsNagDialog: function() {
    // show only once
    if(!this.nagAboutSkippedQuestions) return false;
    this.nagAboutSkippedQuestions = false;

    if(this.answersGiven.skip.length === 0) return false;

    var code = '
'
      + '
Übersprungene Fragen
'
      + '
Du hast ' + this.answersGiven.skip.length + ' Frage(n) übersprungen. Sollen sie nochmal angezeigt werden, oder möchtest Du abschließen?
'
      + '
'
      + 'Block abschließen'
      + 'nochmal vorlegen'
      + '
';

    $(code).appendTo('body').animate(CONST.showAnimation, CONST.stayAtBottom);
    return true;
  },

  _showFinishDialog: function() {
    if(this._showSkippedQuestionsNagDialog()) return;

    var sum = this.answersGiven.correct.length + this.answersGiven.fail.length;

    var code = '
'
      + '
Fertig!
'
      + '
Du hast den aktuellen Block abgeschlossen. Insgesamt hast Du '+sum+' Fragen beantwortet und davon '+this.answersGiven.fail.length+' falsch. Scrolle nach oben um jeweils die Antworten für die Fragen zu sehen.
'
      + '
'
      + 'Gib mir nochmal '+$('#quantity').val()+'!'
      + 'Einstellungen ändern'
      + '
';

    if($('#comiccheckbox').is(':checked')) {
      code = code
        + '

Zur Belohnung ist hier ein zufälliges XKCD-Comic:
'
        + '

'
        + '
(eigentlich müsste hier ein Link auf XKCD stehen – zu Deinem Schutz fehlt er aber)'
        + '
';
    }

    $(code).appendTo('body').animate(CONST.showAnimation, CONST.stayAtBottom);

    animateVisibilityHiddenShow($('.reveal .answer-chooser > div > span:nth-child(2), .reveal .initiallyHidden'));

    var allCorr = this.answersGiven.correct.diff(this.answersGiven.fail).join(',');
    var anyFail = this.answersGiven.fail.join(',');
    $(allCorr).addClass('correct');
    $(anyFail).addClass('wrong');

    $('.xkcd:last').load(Routes.random_xkcd_path() + ' #comic');
  },

  showNext: function() {
    if(this.questions.length === 0) {
      alert("Keine Fragen für die aktuellen Einstellungen gefunden.\n\nVersuche es mit anderen Einstellungen nochmal.");
      enableOptions();
      showAllCategories();
    // no more available questions?
    } else if(this.questions.length-1 === this.questPositionPointer) {
      this._showFinishDialog();
    // reached user defined question limit
    } else if(answersGivenCount >= parseInt($('#quantity').val())) {
      this._showFinishDialog();
    } else {
      this._showNextQuestion();
    }
  },

  skipCurrentQuestion: function(self) {
    $(self).animate(CONST.hideAnimation);
    this.showNext();
  }
}

// convenience generator
H.hitme = function (categoryElement) {
  var h = new H.Hitme(categoryElement);
  window.currentHitme = h;
  return h;
}
<|endoftext|>


<commit_before>import React          from 'react';
import TestUtils      from 'react-addons-test-utils';
import halcyonReducer from '../halcyon';
import Immutable      from 'immutable';
import {
  createWizard,
  destroyWizard,
  setWizardModel,
  changeWizardStep,
  openWizard
} from '../../actions/wizard';

const reducer = (function (reducer, _state) {
  const runWithState = (state, action) => _state = reducer(state, action);

  return {
    run    : (action) => runWithState(_state, action),
    runWithState : runWithState,
    resetState   : runWithState.bind(null, undefined, {}),
    getState     : () => _state
  };
})(halcyonReducer);

describe('(Reducer) Halcyon', function () {

  beforeEach(function () {
    reducer.resetState();
  });

  describe('Initial State', function () {

    it('Should be an instance of Immutable.List.', function () {
      expect(Immutable.List.isList(reducer.getState())).to.be.true;
    });

    it('Should be an empty list.', function () {
      expect(reducer.getState().size).to.equal(0);
    });
  });

  describe('HALCYON_WIZARD_CREATE', function () {

    it('Should return a new state reference.', function () {
      const before = reducer.getState();
      const after  = reducer.run(createWizard());

      expect(before).to.not.equal(after);
    });

    it('Should initialize the new entry as an Immutable.Map.', function () {
      const state = reducer.run(createWizard());

      expect(Immutable.Map.isMap(state.get(0))).to.be.true;
    });

    describe('Initialized Properties', function () {
      var _mockInstance, _mockModel, _item;

      beforeEach(function () {
        _mockInstance = {};
        _mockModel = {};

        _item = reducer.run(createWizard(_mockInstance, _mockModel)).first();
      });

      it('Should define a property "instance".', function () {
        expect(_item.get('instance')).to.be.defined;
      });

      it('Should reference the "instance" property provided in the action payload.', function () {
        expect(_item.get('instance')).to.equal(_mockInstance);
      });

      it('Should define a property "stepIndex".', function () {
        expect(_item.get('stepIndex')).to.be.defined;
      });

      it('Should initialize the property "stepIndex" to 0.', function () {
        expect(_item.get('stepIndex')).to.be.a.number;
        expect(_item.get('stepIndex')).to.equal(0);
      });

      it('Should define a property "depth".', function () {
        expect(_item.get('depth')).to.be.defined;
      });

      it('Should define a property model.', function () {
        expect(_item.get('model')).to.be.defined;
      });

      it('Should reference the "model" property provided in action payload.', function () {
        expect(_item.get('model')).to.equal(_mockModel);
      });
    });

    it('Should insert a new item in the current list of wizards.', function () {
      const before = reducer.getState();
      const after  = reducer.run(createWizard());

      expect(after.size).to.equal(1);
    });

    it('Should insert the new item to the back of the list.', function () {
      const first = reducer.run(createWizard()).first();
      const afterSecondAdd = reducer.run(createWizard());

      expect(afterSecondAdd.size).to.equal(2);
      expect(afterSecondAdd.get(1)).to.be.defined;
      expect(afterSecondAdd.get(0)).to.equal(first);
      expect(afterSecondAdd.get(1)).to.not.equal(first);
    });
  });

  describe('HALCYON_WIZARD_DESTROY', function () {
    var _a, _b, _c; // mock instances

    beforeEach(function () {
      reducer.run(createWizard(_a = {}));
      reducer.run(createWizard(_b = {}));
      reducer.run(createWizard(_c = {}));
    });

    it('(meta) Should initialize test with 3 mock wizards.', function () {
      expect(reducer.getState().size).to.equal(3);
    });

    it('Should return a new state reference.', function () {
      const before = reducer.getState();

      expect(reducer.run(destroyWizard(_b))).to.not.equal(before);
    });

    it('Should return the same state reference if the target instance is not found.', function () {
      const before = reducer.getState();
      const after  = reducer.run(destroyWizard());

      expect(after.size).to.equal(3);
      expect(after).to.equal(before);
    });

  });

  describe('HALCYON_WIZARD_SET_MODEL', function () {

  });

  describe('HALCYON_WIZARD_STEP_CHANGE', function () {

  });
});
<commit_msg>test(reducer): adds initial tests for WIZARD_SET_MODEL and WIZARD_DESTROY<commit_after>import React          from 'react';
import TestUtils      from 'react-addons-test-utils';
import halcyonReducer from '../halcyon';
import Immutable      from 'immutable';
import {
  createWizard,
  destroyWizard,
  setWizardModel,
  changeWizardStep,
  openWizard
} from '../../actions/wizard';

const reducer = (function (reducer, _state) {
  const runWithState = (state, action) => _state = reducer(state, action);

  return {
    run    : (action) => runWithState(_state, action),
    runWithState : runWithState,
    resetState   : runWithState.bind(null, undefined, {}),
    getState     : () => _state
  };
})(halcyonReducer);

describe('(Reducer) Halcyon', function () {

  beforeEach(function () {
    reducer.resetState();
  });

  describe('Initial State', function () {

    it('Should be an instance of Immutable.List.', function () {
      expect(Immutable.List.isList(reducer.getState())).to.be.true;
    });

    it('Should be an empty list.', function () {
      expect(reducer.getState().size).to.equal(0);
    });
  });

  describe('HALCYON_WIZARD_CREATE', function () {

    it('Should return a new state reference.', function () {
      const before = reducer.getState();
      const after  = reducer.run(createWizard());

      expect(before).to.not.equal(after);
    });

    it('Should initialize the new entry as an Immutable.Map.', function () {
      const state = reducer.run(createWizard());

      expect(Immutable.Map.isMap(state.get(0))).to.be.true;
    });

    describe('Initialized Properties', function () {
      var _mockInstance, _mockModel, _item;

      beforeEach(function () {
        _mockInstance = {};
        _mockModel = {};

        _item = reducer.run(createWizard(_mockInstance, _mockModel)).first();
      });

      it('Should define a property "instance".', function () {
        expect(_item.get('instance')).to.be.defined;
      });

      it('Should reference the "instance" property provided in the action payload.', function () {
        expect(_item.get('instance')).to.equal(_mockInstance);
      });

      it('Should define a property "stepIndex".', function () {
        expect(_item.get('stepIndex')).to.be.defined;
      });

      it('Should initialize the property "stepIndex" to 0.', function () {
        expect(_item.get('stepIndex')).to.be.a.number;
        expect(_item.get('stepIndex')).to.equal(0);
      });

      it('Should define a property "depth".', function () {
        expect(_item.get('depth')).to.be.defined;
      });

      it('Should define the property depth as item\'s index in the list of wizards.', function () {
        expect(_item.get('depth')).to.equal(0);

        expect(reducer.run(createWizard()).get(1).get('depth')).to.equal(1);
        expect(reducer.run(createWizard()).get(2).get('depth')).to.equal(2);
      });

      it('Should define a property model.', function () {
        expect(_item.get('model')).to.be.defined;
      });

      it('Should reference the "model" property provided in action payload.', function () {
        expect(_item.get('model')).to.equal(_mockModel);
      });
    });

    it('Should insert a new item in the current list of wizards.', function () {
      const before = reducer.getState();
      const after  = reducer.run(createWizard());

      expect(after.size).to.equal(1);
    });

    it('Should insert the new item to the back of the list.', function () {
      const first = reducer.run(createWizard()).first();
      const afterSecondAdd = reducer.run(createWizard());

      expect(afterSecondAdd.size).to.equal(2);
      expect(afterSecondAdd.get(1)).to.be.defined;
      expect(afterSecondAdd.get(0)).to.equal(first);
      expect(afterSecondAdd.get(1)).to.not.equal(first);
    });
  });

  describe('HALCYON_WIZARD_DESTROY', function () {
    var _a, _b, _c; // mock instances

    beforeEach(function () {
      reducer.run(createWizard(_a = {}));
      reducer.run(createWizard(_b = {}));
      reducer.run(createWizard(_c = {}));
    });

    it('(meta) Should initialize test with 3 mock wizards.', function () {
      expect(reducer.getState().size).to.equal(3);
    });

    it('Should return a new state reference.', function () {
      const before = reducer.getState();

      expect(reducer.run(destroyWizard(_b))).to.not.equal(before);
    });

    it('Should return the same state reference if the target instance is not found.', function () {
      const before = reducer.getState();
      const after  = reducer.run(destroyWizard());

      expect(after.size).to.equal(3);
      expect(after).to.equal(before);
    });

    it('Should delete the state list entry whose property "instance" matches the target instance.', function () {
      const before   = reducer.getState();
      const deletedA = reducer.run(destroyWizard(_a));

      // Delete _a
      expect(deletedA.size).to.equal(2);
      expect(deletedA.find(w => w.get('instance') === _a)).to.be.undefined;
      expect(deletedA.find(w => w.get('instance') === _b)).to.be.defined;
      expect(deletedA.find(w => w.get('instance') === _c)).to.be.defined;

      // Delete _b
      const deletedB = reducer.run(destroyWizard(_b));
      expect(deletedB.size).to.equal(1);
      expect(deletedB.find(w => w.get('instance') === _a)).to.be.undefined;
      expect(deletedB.find(w => w.get('instance') === _b)).to.be.undefined;
      expect(deletedB.find(w => w.get('instance') === _c)).to.be.defined;

      // Attempt to delete _b again
      const deleteBAgain = reducer.run(destroyWizard(_b));
      expect(deleteBAgain.size).to.equal(1);
      expect(deleteBAgain.find(w => w.get('instance') === _a)).to.be.undefined;
      expect(deleteBAgain.find(w => w.get('instance') === _b)).to.be.undefined;
      expect(deleteBAgain.find(w => w.get('instance') === _c)).to.be.defined;
    });

  });

  describe('HALCYON_WIZARD_SET_MODEL', function () {
    var _a, _b, _c; // mock instances

    beforeEach(function () {
      reducer.run(createWizard(_a = {}));
      reducer.run(createWizard(_b = {}));
      reducer.run(createWizard(_c = {}));
    });

    it('Should set the supplied model on the entry that matches the target instance.', function () {
      const beforeModel = reducer.getState().first().get('model');
      const newModel    = {};
      const afterModel  = reducer.run(setWizardModel(_a, newModel)).first().get('model');

      expect(afterModel).to.not.equal(beforeModel);
      expect(afterModel).to.equal(newModel);
    });

    it('Should return a new state reference.', function () {
      const before = reducer.getState();
      const after  = reducer.run(setWizardModel(_a, {}));

      expect(before).to.not.equal(after);
    });

    it('Should not affect the models of the other entries.', function () {

      // models from the old state
      const aModel   = reducer.getState().get(0).get('model');
      const bModel   = reducer.getState().get(1).get('model');
      const cModel   = reducer.getState().get(2).get('model');
      const newModel = {};

      // models from the new state
      const after       = reducer.run(setWizardModel(_a, newModel));
      const afterAModel = after.get(0).get('model');
      const afterBModel = after.get(1).get('model');
      const afterCModel = after.get(2).get('model');

      expect(afterAModel).to.equal(newModel);
      expect(afterAModel).to.not.equal(aModel);

      expect(afterBModel).to.equal(bModel);
      expect(afterCModel).to.equal(cModel);
    });
  });

  describe('HALCYON_WIZARD_STEP_CHANGE', function () {

  });
});
<|endoftext|>


<commit_before>function validateNumberOnly(self) {
  self = $(self);
  var val = parseInt(self.val().replace(/[^0-9]/g, "") || 10);
  var max = parseInt(self.attr('max') || 999999);
  var min = parseInt(self.attr('min') ||-999999);
  self.val(Math.max(min, Math.min(max, val)));
}

function parseMatrix(orig) {
  var s = $.trim(orig);
  var rows = s.split(/[\r\n]+/);
  rows = $.map(rows, function(r) {
    return $.trim(r).split(/\s+/).join(" ");
  });
  return rows.join("  ");
}

function showNextHint(elm) {
  var hidden = $(elm).siblings(":hidden");
  hidden.first().animate(CONST.showAnimation);
  if(hidden.length === 1) $(elm).animate(CONST.hideAnimation);
}

function renderStarred(question) {
  if(!window.loggedIn) return '';
  c = "";
  c += '
';

  c += question.starred ? '★ ' : '';
  c += 'Frage ';
  c += question.starred ? 'gemerkt' : 'merken';
  c += '
';

  return c;
}

function handleStarredClick(link) {
  var l = $(link);
  var s = l.data('starred');
  var id = l.data('id');
  var r = s ? Routes.unstar_question_path(id) : Routes.star_question_path(id);

  $.ajax({
    url: r,
  }).done(function(data) {
    var fakeQ = {id: id, starred: data};
    l.parent().replaceWith(renderStarred(fakeQ));
  }).fail(function() {
    l.text('irgendwas ist kaputt…').addClass('disable');
  });
}

function getQuestionById(id) {
  var quest = null;
  $.each(window.currentHitme.questions, function(ind, q) {
    if(q.id === id) {
      quest = q;
      return false;
    }
  });
  return quest;
}

function storeStats(quest_id, answ_id) {
  $.post(Routes.new_stat_path(quest_id, answ_id), {a: "b"});
}

function ensureValidDifficultySelection() {
  var any = false;
  $('input[type=checkbox][name^=difficulty]').each(function() {
    if($(this).is('checked')) {
      any = true;
      return false;
    }
  });
  if(!any) $('input[type=checkbox][name^=difficulty]:first').prop('checked', true);
}

function disableOptions()  {
  $('#options input').attr("disabled", "disabled");
  $('#options').animate(CONST.hideAnimation);
}

function enableOptions() {
  $('#options input').removeAttr("disabled");
}

function hideCategories() {
  $('h3 + .toggle:visible').each(function(i, cat) {
    $(cat).prev().click();
  });
}

function getDifficulties() {
  var diff = [];
  $('input[type=checkbox][name^=difficulty]').each(function() {
    if(this.checked) {
      diff.push($(this).val());
    }
  });
  return diff.join("_");
}

function getStudyPath() {
  var diff = [];
  $('input[type=checkbox][name^=difficulty]').each(function() {
    if(this.checked) {
      diff.push($(this).val());
    }
  });
  return diff.join("_");
}

function showAllCategories() {
  $('#categories li').animate(CONST.showAnimation);
  var s = $('a.disable');
  s.removeClass('disable').attr('onclick', s.data('oldonclick'));
}

function animateVisibilityHiddenShow(elms) {
  elms.css('visibility','visible').hide().fadeIn('slow');
}

function getURLForRootQuestions(categoryIds) {
  var diff = this.getDifficulties();
  var count = $('#quantity').val();
  var studyPath = $('#study_path').val();

  var h = {categories: categoryIds.join("_"), count: count, difficulty: diff, study_path: studyPath}

  return Routes.main_question_path(h);
}

function getRootQuestions(categoryIds, context, successCallback) {
  // example: http://0.0.0.0:3000/main/questions?categories=6_8&count=10&difficulty=10_20_30&study_path=3
  $.ajax({
    url: getURLForRootQuestions(categoryIds),
  }).done(function(data) {
    successCallback(context, data);
  }).fail(function() {
    alert("Die Anfrage konnte leider nicht bearbeitet werden. Möglicherweise hat der Server ein Problem.");
  });
}

function answersGivenCount() {
  var a = window.H.answersGiven;
  return a.fail.length + a.correct.length + a.skip.length;
}

function maybeInsertSubquestion(aid) {
  // only try to show subquestion half of the time
  //~ if(Math.random() < 0.5) return; // fix as per #5
  var q = window.currentQuestion;
  var s;
  $.each(q.answers, function(ind, answ) {
    if(answ.id === parseInt(aid)) {
      s = answ.subquestion;
      return false;
    }
  });
  // this answer doesn’t have a subquestion
  if(!s) return;
  var c = window.currentHitme;
  var p = c.questPositionPointer;
  c.questions.splice(p+1, 0, s);
}

H = {};
window.H = H;

window.currentHitme = null;

Array.prototype.diff = function(a) {
    return this.filter(function(i) {return !(a.indexOf(i) > -1);});
};

window.CONST = {
  hideAnimation: {opacity: 'hide', height: 'hide', marginTop: 'hide',
    marginBottom: 'hide', paddingTop: 'hide', paddingBottom: 'hide'},
  showAnimation: {opacity: 'show', height: 'show', marginTop: 'show',
    marginBottom: 'show', paddingTop: 'show', paddingBottom: 'show'},
  stayAtBottom: { duration: 'slow',
    step: function(now, fx) {
      $('html, body').scrollTop(99999999);
    }
  }
}

// constructor
H.Hitme = function() {
  this.cats = $('.inline-chooser .active').map(function(i, m) { return $(m).data("id"); }).get();
  if(this.cats.length === 0) {
    alert("Wähle bitte eine Kategorie aus.");
    return;
  }

console.log(this.cats);

  this._this = this;
  disableOptions();
  ensureValidDifficultySelection();
  hideCategories();
  getRootQuestions(this.cats, this, this.rootQuestionsAvailable);
  this.questPositionPointer = -1;
  this.answersGiven = {correct: [], fail: [], skip: []};
};

// members
H.Hitme.prototype = {
  giveMore: function() {
    $('.hideMeOnMore').remove();
    H.hitme(this.cat);
  },

  rootQuestionsAvailable: function(_this, data) {
    _this.questions = data;
    _this.showNext();
  },

  _renderAnswersForQuestion: function(quest) {
    var s = "";
    $.each(quest.answers, function(ind, a) {
      s += '
'
      s += ''+a.html+'';
      s += ''+a.correctness+'';
      s += 'Deine Antwort';
      s += '
';
    });
    return s;
  },

  _handleAnswerClick: function() {
    var answ = $(this);
    var linkBox = answ.parents('.answer-chooser, .answer-chooser-matrix').first();
    var boxSelector = '#blockQ' + answ.data('qid');
    var box = $(boxSelector);

    var isMatrix = getQuestionById(answ.data('qid')).matrix;
    if(isMatrix && answ.data('aid') === 1) {
      var txt = box.find("textarea");
      var m = parseMatrix(txt.val());
      txt.attr('disabled', 'disabled');
      var isCorr = window.currentQuestion.matrix_solution === m;
      answ.data('correct', isCorr);
      if(!isCorr) answ.data('aid', 0);
    }

    storeStats(answ.data('qid'), answ.data('aid'));

    var c = answ.data('correct');
    switch(c) {
      case "true":
      case true:
      window.currentHitme.answersGiven.correct.push(boxSelector);
      box.addClass('reveal');
      maybeInsertSubquestion(answ.data('aid'));
      break;

      case "false":
      case false:
      window.currentHitme.answersGiven.fail.push(boxSelector);
      box.addClass('reveal');
      maybeInsertSubquestion(answ.data('aid'));
      break;

      default:
      window.currentHitme.answersGiven.skip.push(boxSelector);
    }

    linkBox.find('a').addClass('disable');
    animateVisibilityHiddenShow(answ.siblings().last());
    window.currentHitme.showNext();
  },

  _showNextQuestion: function() {
    this.questPositionPointer++;
    var q = window.currentQuestion = this.questions[this.questPositionPointer];

    var code = '
'
      + q.html
      + '
';

    if(q.hints.length >= 1) {
      code += '
'
      $.each(q.hints, function(ind, hint) {
        code += '
'+hint+'
';
      });
      code += 'Hinweis anzeigen';
      code += '
';
    }

    code += renderStarred(q);

    var cls;

    if(q.matrix) {
      cls = 'answer-chooser-matrix';

      code += 'Trage unten die Lösung ein. Matrizen schreibst Du einfach mittels Leerzeichen und Zeilenumbrüchen. Die Anzahl der Leerzeichen ist dabei egal.

'
      var a = q.answers[0];
      code += '
';
      code += 'Deine Lösung';
      code += '<textarea id="a'+a.id+'" class="matrixmode"></textarea>
';
      code += '
';
      code += '
';
      code += 'Unsere Lösung
';
      code += a.html+'
';
      code += '
';
      code += '
';
      code += 'Antwort übernehmen';
      code += 'Frage überspringen';
      code += '
';
    } else {
      cls = 'answer-chooser';
      code += '
'
        + this._renderAnswersForQuestion(q)
        +'
Frage überspringenDu hast diese Frage übersprungen
'
        + '
';
    }

    code += '
';

    $(code).appendTo('body').animate(CONST.showAnimation, CONST.stayAtBottom);
    $('.'+cls+':last').one('click', 'a', this._handleAnswerClick);
  },

  _showFinishDialog: function() {
    var sum = this.answersGiven.correct.length + this.answersGiven.fail.length;

    var code = '
'
      + '
Fertig!
'
      + '
Du hast den aktuellen Block abgeschlossen. Insgesamt hast Du '+sum+' Fragen beantwortet und davon '+this.answersGiven.fail.length+' falsch. Scrolle nach oben um jeweils die Antworten für die Fragen zu sehen.
'
      + '
'
      + 'Gib mir nochmal '+$('#quantity').val()+'!'
      + 'Einstellungen ändern'
      + '
'
      + '

Zur Belohnung ist hier ein zufälliges XKCD-Comic:
'
      + '

'
      + '
(eigentlich müsste hier ein Link auf XKCD stehen – zu Deinem Schutz fehlt er aber)'
      + '
';

    $(code).appendTo('body').animate(CONST.showAnimation, CONST.stayAtBottom);

    animateVisibilityHiddenShow($('.reveal .answer-chooser > div > span:nth-child(2), .reveal .initiallyHidden'));

    var allCorr = this.answersGiven.correct.diff(this.answersGiven.fail).join(',');
    var anyFail = this.answersGiven.fail.join(',');
    $(allCorr).addClass('correct');
    $(anyFail).addClass('wrong');

    $('.xkcd:last').load(Routes.random_xkcd_path() + ' #comic');
  },

  showNext: function() {
    if(this.questions.length === 0) {
      alert("Keine Fragen für die aktuellen Einstellungen gefunden.\n\nVersuche es mit anderen Einstellungen nochmal.");
      enableOptions();
      showAllCategories();
    // no more available questions?
    } else if(this.questions.length-1 === this.questPositionPointer) {
      this._showFinishDialog();
    // reached user defined question limit
    } else if(answersGivenCount >= parseInt($('#quantity').val())) {
      this._showFinishDialog();
    } else {
      this._showNextQuestion();
    }
  },

  skipCurrentQuestion: function(self) {
    $(self).animate(CONST.hideAnimation);
    this.showNext();
  }
}

// convenience generator
H.hitme = function (categoryElement) {
  var h = new H.Hitme(categoryElement);
  window.currentHitme = h;
  return h;
}
<commit_msg>clear debug output<commit_after>function validateNumberOnly(self) {
  self = $(self);
  var val = parseInt(self.val().replace(/[^0-9]/g, "") || 10);
  var max = parseInt(self.attr('max') || 999999);
  var min = parseInt(self.attr('min') ||-999999);
  self.val(Math.max(min, Math.min(max, val)));
}

function parseMatrix(orig) {
  var s = $.trim(orig);
  var rows = s.split(/[\r\n]+/);
  rows = $.map(rows, function(r) {
    return $.trim(r).split(/\s+/).join(" ");
  });
  return rows.join("  ");
}

function showNextHint(elm) {
  var hidden = $(elm).siblings(":hidden");
  hidden.first().animate(CONST.showAnimation);
  if(hidden.length === 1) $(elm).animate(CONST.hideAnimation);
}

function renderStarred(question) {
  if(!window.loggedIn) return '';
  c = "";
  c += '
';

  c += question.starred ? '★ ' : '';
  c += 'Frage ';
  c += question.starred ? 'gemerkt' : 'merken';
  c += '
';

  return c;
}

function handleStarredClick(link) {
  var l = $(link);
  var s = l.data('starred');
  var id = l.data('id');
  var r = s ? Routes.unstar_question_path(id) : Routes.star_question_path(id);

  $.ajax({
    url: r,
  }).done(function(data) {
    var fakeQ = {id: id, starred: data};
    l.parent().replaceWith(renderStarred(fakeQ));
  }).fail(function() {
    l.text('irgendwas ist kaputt…').addClass('disable');
  });
}

function getQuestionById(id) {
  var quest = null;
  $.each(window.currentHitme.questions, function(ind, q) {
    if(q.id === id) {
      quest = q;
      return false;
    }
  });
  return quest;
}

function storeStats(quest_id, answ_id) {
  $.post(Routes.new_stat_path(quest_id, answ_id), {a: "b"});
}

function ensureValidDifficultySelection() {
  var any = false;
  $('input[type=checkbox][name^=difficulty]').each(function() {
    if($(this).is('checked')) {
      any = true;
      return false;
    }
  });
  if(!any) $('input[type=checkbox][name^=difficulty]:first').prop('checked', true);
}

function disableOptions()  {
  $('#options input').attr("disabled", "disabled");
  $('#options').animate(CONST.hideAnimation);
}

function enableOptions() {
  $('#options input').removeAttr("disabled");
}

function hideCategories() {
  $('h3 + .toggle:visible').each(function(i, cat) {
    $(cat).prev().click();
  });
}

function getDifficulties() {
  var diff = [];
  $('input[type=checkbox][name^=difficulty]').each(function() {
    if(this.checked) {
      diff.push($(this).val());
    }
  });
  return diff.join("_");
}

function getStudyPath() {
  var diff = [];
  $('input[type=checkbox][name^=difficulty]').each(function() {
    if(this.checked) {
      diff.push($(this).val());
    }
  });
  return diff.join("_");
}

function showAllCategories() {
  $('#categories li').animate(CONST.showAnimation);
  var s = $('a.disable');
  s.removeClass('disable').attr('onclick', s.data('oldonclick'));
}

function animateVisibilityHiddenShow(elms) {
  elms.css('visibility','visible').hide().fadeIn('slow');
}

function getURLForRootQuestions(categoryIds) {
  var diff = this.getDifficulties();
  var count = $('#quantity').val();
  var studyPath = $('#study_path').val();

  var h = {categories: categoryIds.join("_"), count: count, difficulty: diff, study_path: studyPath}

  return Routes.main_question_path(h);
}

function getRootQuestions(categoryIds, context, successCallback) {
  // example: http://0.0.0.0:3000/main/questions?categories=6_8&count=10&difficulty=10_20_30&study_path=3
  $.ajax({
    url: getURLForRootQuestions(categoryIds),
  }).done(function(data) {
    successCallback(context, data);
  }).fail(function() {
    alert("Die Anfrage konnte leider nicht bearbeitet werden. Möglicherweise hat der Server ein Problem.");
  });
}

function answersGivenCount() {
  var a = window.H.answersGiven;
  return a.fail.length + a.correct.length + a.skip.length;
}

function maybeInsertSubquestion(aid) {
  // only try to show subquestion half of the time
  //~ if(Math.random() < 0.5) return; // fix as per #5
  var q = window.currentQuestion;
  var s;
  $.each(q.answers, function(ind, answ) {
    if(answ.id === parseInt(aid)) {
      s = answ.subquestion;
      return false;
    }
  });
  // this answer doesn’t have a subquestion
  if(!s) return;
  var c = window.currentHitme;
  var p = c.questPositionPointer;
  c.questions.splice(p+1, 0, s);
}

H = {};
window.H = H;

window.currentHitme = null;

Array.prototype.diff = function(a) {
    return this.filter(function(i) {return !(a.indexOf(i) > -1);});
};

window.CONST = {
  hideAnimation: {opacity: 'hide', height: 'hide', marginTop: 'hide',
    marginBottom: 'hide', paddingTop: 'hide', paddingBottom: 'hide'},
  showAnimation: {opacity: 'show', height: 'show', marginTop: 'show',
    marginBottom: 'show', paddingTop: 'show', paddingBottom: 'show'},
  stayAtBottom: { duration: 'slow',
    step: function(now, fx) {
      $('html, body').scrollTop(99999999);
    }
  }
}

// constructor
H.Hitme = function() {
  this.cats = $('.inline-chooser .active').map(function(i, m) { return $(m).data("id"); }).get();
  if(this.cats.length === 0) {
    alert("Wähle bitte eine Kategorie aus.");
    return;
  }

  this._this = this;
  disableOptions();
  ensureValidDifficultySelection();
  hideCategories();
  getRootQuestions(this.cats, this, this.rootQuestionsAvailable);
  this.questPositionPointer = -1;
  this.answersGiven = {correct: [], fail: [], skip: []};
};

// members
H.Hitme.prototype = {
  giveMore: function() {
    $('.hideMeOnMore').remove();
    H.hitme(this.cat);
  },

  rootQuestionsAvailable: function(_this, data) {
    _this.questions = data;
    _this.showNext();
  },

  _renderAnswersForQuestion: function(quest) {
    var s = "";
    $.each(quest.answers, function(ind, a) {
      s += '
'
      s += ''+a.html+'';
      s += ''+a.correctness+'';
      s += 'Deine Antwort';
      s += '
';
    });
    return s;
  },

  _handleAnswerClick: function() {
    var answ = $(this);
    var linkBox = answ.parents('.answer-chooser, .answer-chooser-matrix').first();
    var boxSelector = '#blockQ' + answ.data('qid');
    var box = $(boxSelector);

    var isMatrix = getQuestionById(answ.data('qid')).matrix;
    if(isMatrix && answ.data('aid') === 1) {
      var txt = box.find("textarea");
      var m = parseMatrix(txt.val());
      txt.attr('disabled', 'disabled');
      var isCorr = window.currentQuestion.matrix_solution === m;
      answ.data('correct', isCorr);
      if(!isCorr) answ.data('aid', 0);
    }

    storeStats(answ.data('qid'), answ.data('aid'));

    var c = answ.data('correct');
    switch(c) {
      case "true":
      case true:
      window.currentHitme.answersGiven.correct.push(boxSelector);
      box.addClass('reveal');
      maybeInsertSubquestion(answ.data('aid'));
      break;

      case "false":
      case false:
      window.currentHitme.answersGiven.fail.push(boxSelector);
      box.addClass('reveal');
      maybeInsertSubquestion(answ.data('aid'));
      break;

      default:
      window.currentHitme.answersGiven.skip.push(boxSelector);
    }

    linkBox.find('a').addClass('disable');
    animateVisibilityHiddenShow(answ.siblings().last());
    window.currentHitme.showNext();
  },

  _showNextQuestion: function() {
    this.questPositionPointer++;
    var q = window.currentQuestion = this.questions[this.questPositionPointer];

    var code = '
'
      + q.html
      + '
';

    if(q.hints.length >= 1) {
      code += '
'
      $.each(q.hints, function(ind, hint) {
        code += '
'+hint+'
';
      });
      code += 'Hinweis anzeigen';
      code += '
';
    }

    code += renderStarred(q);

    var cls;

    if(q.matrix) {
      cls = 'answer-chooser-matrix';

      code += 'Trage unten die Lösung ein. Matrizen schreibst Du einfach mittels Leerzeichen und Zeilenumbrüchen. Die Anzahl der Leerzeichen ist dabei egal.

'
      var a = q.answers[0];
      code += '
';
      code += 'Deine Lösung';
      code += '<textarea id="a'+a.id+'" class="matrixmode"></textarea>
';
      code += '
';
      code += '
';
      code += 'Unsere Lösung
';
      code += a.html+'
';
      code += '
';
      code += '
';
      code += 'Antwort übernehmen';
      code += 'Frage überspringen';
      code += '
';
    } else {
      cls = 'answer-chooser';
      code += '
'
        + this._renderAnswersForQuestion(q)
        +'
Frage überspringenDu hast diese Frage übersprungen
'
        + '
';
    }

    code += '
';

    $(code).appendTo('body').animate(CONST.showAnimation, CONST.stayAtBottom);
    $('.'+cls+':last').one('click', 'a', this._handleAnswerClick);
  },

  _showFinishDialog: function() {
    var sum = this.answersGiven.correct.length + this.answersGiven.fail.length;

    var code = '
'
      + '
Fertig!
'
      + '
Du hast den aktuellen Block abgeschlossen. Insgesamt hast Du '+sum+' Fragen beantwortet und davon '+this.answersGiven.fail.length+' falsch. Scrolle nach oben um jeweils die Antworten für die Fragen zu sehen.
'
      + '
'
      + 'Gib mir nochmal '+$('#quantity').val()+'!'
      + 'Einstellungen ändern'
      + '
'
      + '

Zur Belohnung ist hier ein zufälliges XKCD-Comic:
'
      + '

'
      + '
(eigentlich müsste hier ein Link auf XKCD stehen – zu Deinem Schutz fehlt er aber)'
      + '
';

    $(code).appendTo('body').animate(CONST.showAnimation, CONST.stayAtBottom);

    animateVisibilityHiddenShow($('.reveal .answer-chooser > div > span:nth-child(2), .reveal .initiallyHidden'));

    var allCorr = this.answersGiven.correct.diff(this.answersGiven.fail).join(',');
    var anyFail = this.answersGiven.fail.join(',');
    $(allCorr).addClass('correct');
    $(anyFail).addClass('wrong');

    $('.xkcd:last').load(Routes.random_xkcd_path() + ' #comic');
  },

  showNext: function() {
    if(this.questions.length === 0) {
      alert("Keine Fragen für die aktuellen Einstellungen gefunden.\n\nVersuche es mit anderen Einstellungen nochmal.");
      enableOptions();
      showAllCategories();
    // no more available questions?
    } else if(this.questions.length-1 === this.questPositionPointer) {
      this._showFinishDialog();
    // reached user defined question limit
    } else if(answersGivenCount >= parseInt($('#quantity').val())) {
      this._showFinishDialog();
    } else {
      this._showNextQuestion();
    }
  },

  skipCurrentQuestion: function(self) {
    $(self).animate(CONST.hideAnimation);
    this.showNext();
  }
}

// convenience generator
H.hitme = function (categoryElement) {
  var h = new H.Hitme(categoryElement);
  window.currentHitme = h;
  return h;
}
<|endoftext|>