<commit_before>let syncDebounce = null;

const cryptographer = new Jose.WebCryptographer();
cryptographer.setKeyEncryptionAlgorithm('A256KW');
cryptographer.setContentEncryptionAlgorithm('A256GCM');

function shared_key(key) {
  return crypto.subtle.importKey(
    'jwk',
    { kty: key.kty, k: key.k.replace(/=/, '') },
    'AES-KW',
    true,
    ['wrapKey', 'unwrapKey']
  );
}

function encrypt(key, content) {
  const encrypter = new JoseJWE.Encrypter(cryptographer, shared_key(key));
  return encrypter.encrypt(JSON.stringify(content));
}

function decrypt(key, encrypted) {
  const decrypter = new JoseJWE.Decrypter(cryptographer, shared_key(key));
  return decrypter.decrypt(encrypted).then(result => {
    return JSON.parse(result);
  });
}

// An "id schema" used to validate Kinto IDs and generate new ones.
const notesIdSchema = { // eslint-disable-line no-unused-vars
  // We do not generate ID to keep retrocompatibility with single note version.
  generate() {
    throw new Error('cannot generate IDs');
  },

  validate() {
    return true;
  },
};

class ServerKeyNewerError extends Error {
  constructor() {
    super('key used to encrypt the record appears to be newer than our key');
  }
}

class ServerKeyOlderError extends Error {
  constructor() {
    super('key used to encrypt the record appears to be older than our key');
  }
}

const deletedNotesStillOnServer = {};

class JWETransformer {
  constructor(key) {
    this.key = key;
  }

  async encode(record) {
    // FIXME: should we try to obfuscate the record ID?
    const ciphertext = await encrypt(this.key, record);
    // Copy over the _status field, so that we handle concurrency
    // headers (If-Match, If-None-Match) correctly.
    const encryptedResult = {
      content: ciphertext,
      id: record.id,
      _status: record._status,
      kid: this.key.kid,
    };
    if (record.hasOwnProperty('last_modified')) {
      encryptedResult.last_modified = record.last_modified;
    }

    return encryptedResult;
  }

  async decode(record) {
    if (!record.content) {
      // This can happen for tombstones if a record is deleted.
      if (record.deleted) {
        return record;
      }
      throw new Error('No ciphertext: nothing to decrypt?');
    }

    if (record.kid !== this.key.kid) {
      if (this.key.kid < record.kid) {
        throw new ServerKeyNewerError();
      } else {
        throw new ServerKeyOlderError();
      }
    }

    const decoded = await decrypt(this.key, record.content);
    if (record.hasOwnProperty('last_modified')) {
      decoded.last_modified = record.last_modified;
    }

    // _status: deleted records were deleted on a client, but
    // uploaded as an encrypted blob so we don't leak deletions.
    // If we get such a record, flag it as deleted.
    if (decoded._status === 'deleted') {
      decoded.deleted = true;
      // On decode, we flag notes with _status deleted but still on server.
      // We automatically will request deletion for those.
      // (This is due to singleNote replacing 'deleted' state by 'updated')
      // Should be deleted when every user who tried beta runned it once.
      // (see metrics deleteDeleted)
      deletedNotesStillOnServer[decoded.id] = decoded;
    }
    return decoded;
  }
}

/**
 * Interface describing a mechanism to fetch credentials.
 */
class Credentials {
  async get() {
    return Promise.reject('Implement me');
  }

  /**
   * Call this if, for example, credentials were invalid.
   */
  async clear() {
    return Promise.reject('Implement me');
  }
}

class BrowserStorageCredentials extends Credentials { // eslint-disable-line no-unused-vars
  constructor(storage) {
    super();
    this.storage = storage;
  }

  async get() {
    const data = await this.storage.get(['credentials']);
    return data.credentials;
  }

  async set(credentials) {
    return this.storage.set({credentials});
  }

  async clear() {
    return this.storage.remove('credentials');
  }
}

/**
 * Try to sync our data against the Kinto server.
 *
 * Returns a promise. The promise can reject in case of sync failure
 * or any other reason. This is so that programming errors can be
 * caught more easily in testing. Since this application is
 * offline-first, sync failure should not be a failure for callers.
 */
function syncKinto(client, credentials) {
  // Get credentials and lastmodified
  let collection, credential;
  return credentials.get()
    .then(received => {
      credential = received;

      if (!received) return;

      return fxaRenewCredential(credential)
        .then((renewedCred) => {
          credential = renewedCred;
          return credentials.set(renewedCred);
        })
        .then(() => {
          // Query Kinto with the Bearer Token
          collection = client
            .collection('notes', {
              idSchema: notesIdSchema,
              remoteTransformers: [new JWETransformer(credential.key)],
            });
          return collection
            .sync({
              headers: {Authorization: `Bearer ${credential.access_token}`},
              strategy: 'manual',
            })
            .catch((error) => {
              if (error.response && error.response.status === 500) {
                // issue #827
                return Promise.resolve({
                  conflicts: []
                });
              }

              throw error;
            });
      });
    })
    .then(syncResult => {
      // FIXME: Do we need to do anything with errors, published,
      // updated, etc.?
      if (syncResult && syncResult.conflicts.length > 0) {
        return Promise.all(syncResult.conflicts.map(conflict => {
          let resolution;
          // If we receive conflict with singleNote, we update
          if (conflict.remote === null) {
            resolution = {
              id: conflict.local.id,
              content: conflict.local.content,
              lastModified: conflict.local.lastModified
            };
          } else {
            resolution = {
              id: conflict.remote.id,
              content: conflict.remote.content
            };

            // If content is different we merge both.
            // Could be difference on lastModified Date.
            if (conflict.remote.content !== conflict.local.content) {
              const mergeWarning = browser.i18n.getMessage('mergeWarning');
              resolution.content =
                `${resolution.content}<p>${mergeWarning}</p>${conflict.local.content}`;
            }

            // We get earlier date for resolved conflict.
            if (conflict.local.lastModified > conflict.remote.lastModified) {
              resolution.lastModified = conflict.local.lastModified;
            } else {
              resolution.lastModified = conflict.remote.lastModified;
            }

            // If they both got deleted we remove them.
            if (conflict.remote.deleted && conflict.local.deleted) {
              resolution.deleted = true;
            }
            client.conflict = true;
            sendMetrics('handle-conflict'); // eslint-disable-line no-undef
          }
          return collection.resolve(conflict, resolution);
        }))
        .then(() => {
          return syncKinto(client, credentials);
        });
      } else if (syncResult === undefined) {
        throw new Error('syncResult is undefined.');
      }
    })
    .catch(error => {
      if (error.response && error.response.status === 401) {
        // In case of 401 log the user out.
        // FIXME: Fetch a new token and retry?
        return reconnectSync(credentials);
      } else if (error instanceof ServerKeyNewerError) {
        // If the key date is greater than current one, log the user out.
        console.error(error); // eslint-disable-line no-console
        return reconnectSync(credentials);
      } else if (error instanceof ServerKeyOlderError) {
        // If the key date is older than the current one, we can't help
        // because there is no way we get the previous key.
        // Flush the server because whatever was there is wrong.
        console.error(error); // eslint-disable-line no-console
        const kintoHttp = client.api;
        return kintoHttp.bucket('default').deleteCollection('notes', {
          headers: { Authorization: `Bearer ${credential.access_token}` }
        }).then(() => collection.resetSyncStatus())
          .then(() => syncKinto(client, credentials));
      } else if (error.message.includes('flushed')) {
        return collection.resetSyncStatus()
          .then(() => {
            return syncKinto(client, credentials);
          });
      } else if (error.message.includes('syncResult is undefined')) {
        return Promise.resolve(null);
      } else if (error.message === 'Failed to renew token') {
        // cannot refresh the access token, log the user out.
        return reconnectSync(credentials);
      } else if (error.response
                && error.response.status === 507
                && error.message.includes('Insufficient Storage')) {

        // cannot refresh the access token, log the user out.
        browser.runtime.sendMessage('notes@mozilla.com', {
          action: 'error',
          message: browser.i18n.getMessage('insufficientStorage')
        });
        return Promise.reject(error);
      }
      console.error(error); // eslint-disable-line no-console
      reconnectSync(credentials);
      return Promise.reject(error);
    });
}

function reconnectSync(credentials) {
  credentials.clear();
  browser.runtime.sendMessage('notes@mozilla.com', {
    action: 'reconnect'
  });
}

function retrieveNote(client) {
  return client
    .collection('notes', { idSchema: notesIdSchema })
    .list({})
    .then((list) => {
      // We delete all notes retrieved from server and not properly deleted
      Object.keys(deletedNotesStillOnServer).forEach((id) => {
        sendMetrics('delete-deleted-notes'); // eslint-disable-line no-undef
        client.collection('notes', { idSchema: notesIdSchema }).deleteAny(id);
      });

      return list;
    });
}

/**
 * Try to sync against the Kinto server, and retrieve the current note
 * contents.
 *
 * On completion, a 'kinto-loaded' event will be fired with the
 * following structure:
 *
 * {
 *   action: 'kinto-loaded',
 *   data: the "content" that was previously saved to Kinto, or null
 *     if nothing was previously saved to Kinto (for example, a new
 *     FxA account, or if syncing failed on a fresh profile)
 *   last_modified: the timestamp of the sync, or null
 * }
 */
function loadFromKinto(client, credentials) { // eslint-disable-line no-unused-vars
  return syncKinto(client, credentials)
    // Ignore failure of syncKinto by retrieving note even when promise rejected
    .then(() => retrieveNote(client), () => retrieveNote(client))
    .then(result => {
      browser.runtime.sendMessage({
        action: 'kinto-loaded',
        notes: result.data,
        last_modified: null
      });
    })
    .catch((e) => {
      browser.runtime.sendMessage({
        action: 'kinto-loaded',
        notes: null,
        last_modified: null
      });
      return new Promise((resolve) => resolve());
    });
}

function saveToKinto(client, credentials, note, fromWindowId) { // eslint-disable-line no-unused-vars
  let resolve;
  // We do not store empty notes on server side.
  if (note.content === '') { return Promise.resolve(); }

  const promise = new Promise(thisResolve => {
    resolve = thisResolve;
  });

  browser.runtime.sendMessage('notes@mozilla.com', {
    action: 'text-editing'
  });

  const later = function() {
    syncDebounce = null;
    const notes = client.collection('notes', { idSchema: notesIdSchema });
    return notes.upsert(note)
      .then((res) => {
        browser.runtime.sendMessage('notes@mozilla.com', {
          action: 'text-saved',
          note: res ? res.data : undefined,
          from: fromWindowId
        });
        client.conflict = false;
        return syncKinto(client, credentials);
      })
      .then(() => retrieveNote(client), () => retrieveNote(client))
      .then(result => {
        // Set the status to synced
        return browser.runtime.sendMessage('notes@mozilla.com', {
          action: 'text-synced',
          note: result.data.find((n) => n.id === note.id),
          conflict: client.conflict,
          from: fromWindowId
        });
      })
      .then(() => {
        resolve();
      })
      .catch(result => {
        browser.runtime.sendMessage('notes@mozilla.com', {
          action: 'text-synced',
          note: undefined,
          conflict: client.conflict,
          from: fromWindowId
        });
        resolve();
      });
  };

  clearTimeout(syncDebounce);
  syncDebounce = setTimeout(later, 400);
  return promise;
}

function createNote(client, note) { // eslint-disable-line no-unused-vars
  return client
    .collection('notes', { idSchema: notesIdSchema })
    .create(note, { useRecordId: true });
}

function deleteNote(client, id) { // eslint-disable-line no-unused-vars
  return client.collection('notes', { idSchema: notesIdSchema }).deleteAny(id);
}

function disconnectFromKinto(client) { // eslint-disable-line no-unused-vars
  const notes = client.collection('notes', { idSchema: notesIdSchema });
  return notes.resetSyncStatus();
}

<commit_msg>If our note has no lastModified value, we override it using kinto record last_modified value. (#878)<commit_after>let syncDebounce = null;

const cryptographer = new Jose.WebCryptographer();
cryptographer.setKeyEncryptionAlgorithm('A256KW');
cryptographer.setContentEncryptionAlgorithm('A256GCM');

function shared_key(key) {
  return crypto.subtle.importKey(
    'jwk',
    { kty: key.kty, k: key.k.replace(/=/, '') },
    'AES-KW',
    true,
    ['wrapKey', 'unwrapKey']
  );
}

function encrypt(key, content) {
  const encrypter = new JoseJWE.Encrypter(cryptographer, shared_key(key));
  return encrypter.encrypt(JSON.stringify(content));
}

function decrypt(key, encrypted) {
  const decrypter = new JoseJWE.Decrypter(cryptographer, shared_key(key));
  return decrypter.decrypt(encrypted).then(result => {
    return JSON.parse(result);
  });
}

// An "id schema" used to validate Kinto IDs and generate new ones.
const notesIdSchema = { // eslint-disable-line no-unused-vars
  // We do not generate ID to keep retrocompatibility with single note version.
  generate() {
    throw new Error('cannot generate IDs');
  },

  validate() {
    return true;
  },
};

class ServerKeyNewerError extends Error {
  constructor() {
    super('key used to encrypt the record appears to be newer than our key');
  }
}

class ServerKeyOlderError extends Error {
  constructor() {
    super('key used to encrypt the record appears to be older than our key');
  }
}

const deletedNotesStillOnServer = {};

class JWETransformer {
  constructor(key) {
    this.key = key;
  }

  async encode(record) {
    // FIXME: should we try to obfuscate the record ID?
    const ciphertext = await encrypt(this.key, record);
    // Copy over the _status field, so that we handle concurrency
    // headers (If-Match, If-None-Match) correctly.
    const encryptedResult = {
      content: ciphertext,
      id: record.id,
      _status: record._status,
      kid: this.key.kid,
    };
    if (record.hasOwnProperty('last_modified')) {
      encryptedResult.last_modified = record.last_modified;
    }

    return encryptedResult;
  }

  async decode(record) {
    if (!record.content) {
      // This can happen for tombstones if a record is deleted.
      if (record.deleted) {
        return record;
      }
      throw new Error('No ciphertext: nothing to decrypt?');
    }

    if (record.kid !== this.key.kid) {
      if (this.key.kid < record.kid) {
        throw new ServerKeyNewerError();
      } else {
        throw new ServerKeyOlderError();
      }
    }

    const decoded = await decrypt(this.key, record.content);
    if (record.hasOwnProperty('last_modified')) {
      decoded.last_modified = record.last_modified;
    }

    // If note has no lastModified (like singleNote from v3), we use kinto last_modified value.
    // kinto last_modified is a timestamp
    if (!decoded.lastModified && decoded.last_modified) {
      decoded.lastModified = new Date(decoded.last_modified);
    }

    // _status: deleted records were deleted on a client, but
    // uploaded as an encrypted blob so we don't leak deletions.
    // If we get such a record, flag it as deleted.
    if (decoded._status === 'deleted') {
      decoded.deleted = true;
      // On decode, we flag notes with _status deleted but still on server.
      // We automatically will request deletion for those.
      // (This is due to singleNote replacing 'deleted' state by 'updated')
      // Should be deleted when every user who tried beta runned it once.
      // (see metrics deleteDeleted)
      deletedNotesStillOnServer[decoded.id] = decoded;
    }
    return decoded;
  }
}

/**
 * Interface describing a mechanism to fetch credentials.
 */
class Credentials {
  async get() {
    return Promise.reject('Implement me');
  }

  /**
   * Call this if, for example, credentials were invalid.
   */
  async clear() {
    return Promise.reject('Implement me');
  }
}

class BrowserStorageCredentials extends Credentials { // eslint-disable-line no-unused-vars
  constructor(storage) {
    super();
    this.storage = storage;
  }

  async get() {
    const data = await this.storage.get(['credentials']);
    return data.credentials;
  }

  async set(credentials) {
    return this.storage.set({credentials});
  }

  async clear() {
    return this.storage.remove('credentials');
  }
}

/**
 * Try to sync our data against the Kinto server.
 *
 * Returns a promise. The promise can reject in case of sync failure
 * or any other reason. This is so that programming errors can be
 * caught more easily in testing. Since this application is
 * offline-first, sync failure should not be a failure for callers.
 */
function syncKinto(client, credentials) {
  // Get credentials and lastmodified
  let collection, credential;
  return credentials.get()
    .then(received => {
      credential = received;

      if (!received) return;

      return fxaRenewCredential(credential)
        .then((renewedCred) => {
          credential = renewedCred;
          return credentials.set(renewedCred);
        })
        .then(() => {
          // Query Kinto with the Bearer Token
          collection = client
            .collection('notes', {
              idSchema: notesIdSchema,
              remoteTransformers: [new JWETransformer(credential.key)],
            });
          return collection
            .sync({
              headers: {Authorization: `Bearer ${credential.access_token}`},
              strategy: 'manual',
            })
            .catch((error) => {
              if (error.response && error.response.status === 500) {
                // issue #827
                return Promise.resolve({
                  conflicts: []
                });
              }

              throw error;
            });
      });
    })
    .then(syncResult => {
      // FIXME: Do we need to do anything with errors, published,
      // updated, etc.?
      if (syncResult && syncResult.conflicts.length > 0) {
        return Promise.all(syncResult.conflicts.map(conflict => {
          let resolution;
          // If we receive conflict with singleNote, we update
          if (conflict.remote === null) {
            resolution = {
              id: conflict.local.id,
              content: conflict.local.content,
              lastModified: conflict.local.lastModified
            };
          } else {
            resolution = {
              id: conflict.remote.id,
              content: conflict.remote.content
            };

            // If content is different we merge both.
            // Could be difference on lastModified Date.
            if (conflict.remote.content !== conflict.local.content) {
              const mergeWarning = browser.i18n.getMessage('mergeWarning');
              resolution.content =
                `${resolution.content}<p>${mergeWarning}</p>${conflict.local.content}`;
            }

            // We get earlier date for resolved conflict.
            if (conflict.local.lastModified > conflict.remote.lastModified) {
              resolution.lastModified = conflict.local.lastModified;
            } else {
              resolution.lastModified = conflict.remote.lastModified;
            }

            // If they both got deleted we remove them.
            if (conflict.remote.deleted && conflict.local.deleted) {
              resolution.deleted = true;
            }
            client.conflict = true;
            sendMetrics('handle-conflict'); // eslint-disable-line no-undef
          }
          return collection.resolve(conflict, resolution);
        }))
        .then(() => {
          return syncKinto(client, credentials);
        });
      } else if (syncResult === undefined) {
        throw new Error('syncResult is undefined.');
      }
    })
    .catch(error => {
      if (error.response && error.response.status === 401) {
        // In case of 401 log the user out.
        // FIXME: Fetch a new token and retry?
        return reconnectSync(credentials);
      } else if (error instanceof ServerKeyNewerError) {
        // If the key date is greater than current one, log the user out.
        console.error(error); // eslint-disable-line no-console
        return reconnectSync(credentials);
      } else if (error instanceof ServerKeyOlderError) {
        // If the key date is older than the current one, we can't help
        // because there is no way we get the previous key.
        // Flush the server because whatever was there is wrong.
        console.error(error); // eslint-disable-line no-console
        const kintoHttp = client.api;
        return kintoHttp.bucket('default').deleteCollection('notes', {
          headers: { Authorization: `Bearer ${credential.access_token}` }
        }).then(() => collection.resetSyncStatus())
          .then(() => syncKinto(client, credentials));
      } else if (error.message.includes('flushed')) {
        return collection.resetSyncStatus()
          .then(() => {
            return syncKinto(client, credentials);
          });
      } else if (error.message.includes('syncResult is undefined')) {
        return Promise.resolve(null);
      } else if (error.message === 'Failed to renew token') {
        // cannot refresh the access token, log the user out.
        return reconnectSync(credentials);
      } else if (error.response
                && error.response.status === 507
                && error.message.includes('Insufficient Storage')) {

        // cannot refresh the access token, log the user out.
        browser.runtime.sendMessage('notes@mozilla.com', {
          action: 'error',
          message: browser.i18n.getMessage('insufficientStorage')
        });
        return Promise.reject(error);
      }
      console.error(error); // eslint-disable-line no-console
      reconnectSync(credentials);
      return Promise.reject(error);
    });
}

function reconnectSync(credentials) {
  credentials.clear();
  browser.runtime.sendMessage('notes@mozilla.com', {
    action: 'reconnect'
  });
}

function retrieveNote(client) {
  return client
    .collection('notes', { idSchema: notesIdSchema })
    .list({})
    .then((list) => {
      // We delete all notes retrieved from server and not properly deleted
      Object.keys(deletedNotesStillOnServer).forEach((id) => {
        sendMetrics('delete-deleted-notes'); // eslint-disable-line no-undef
        client.collection('notes', { idSchema: notesIdSchema }).deleteAny(id);
      });

      return list;
    });
}

/**
 * Try to sync against the Kinto server, and retrieve the current note
 * contents.
 *
 * On completion, a 'kinto-loaded' event will be fired with the
 * following structure:
 *
 * {
 *   action: 'kinto-loaded',
 *   data: the "content" that was previously saved to Kinto, or null
 *     if nothing was previously saved to Kinto (for example, a new
 *     FxA account, or if syncing failed on a fresh profile)
 *   last_modified: the timestamp of the sync, or null
 * }
 */
function loadFromKinto(client, credentials) { // eslint-disable-line no-unused-vars
  return syncKinto(client, credentials)
    // Ignore failure of syncKinto by retrieving note even when promise rejected
    .then(() => retrieveNote(client), () => retrieveNote(client))
    .then(result => {
      browser.runtime.sendMessage({
        action: 'kinto-loaded',
        notes: result.data,
        last_modified: null
      });
    })
    .catch((e) => {
      browser.runtime.sendMessage({
        action: 'kinto-loaded',
        notes: null,
        last_modified: null
      });
      return new Promise((resolve) => resolve());
    });
}

function saveToKinto(client, credentials, note, fromWindowId) { // eslint-disable-line no-unused-vars
  let resolve;
  // We do not store empty notes on server side.
  if (note.content === '') { return Promise.resolve(); }

  const promise = new Promise(thisResolve => {
    resolve = thisResolve;
  });

  browser.runtime.sendMessage('notes@mozilla.com', {
    action: 'text-editing'
  });

  const later = function() {
    syncDebounce = null;
    const notes = client.collection('notes', { idSchema: notesIdSchema });
    return notes.upsert(note)
      .then((res) => {
        browser.runtime.sendMessage('notes@mozilla.com', {
          action: 'text-saved',
          note: res ? res.data : undefined,
          from: fromWindowId
        });
        client.conflict = false;
        return syncKinto(client, credentials);
      })
      .then(() => retrieveNote(client), () => retrieveNote(client))
      .then(result => {
        // Set the status to synced
        return browser.runtime.sendMessage('notes@mozilla.com', {
          action: 'text-synced',
          note: result.data.find((n) => n.id === note.id),
          conflict: client.conflict,
          from: fromWindowId
        });
      })
      .then(() => {
        resolve();
      })
      .catch(result => {
        browser.runtime.sendMessage('notes@mozilla.com', {
          action: 'text-synced',
          note: undefined,
          conflict: client.conflict,
          from: fromWindowId
        });
        resolve();
      });
  };

  clearTimeout(syncDebounce);
  syncDebounce = setTimeout(later, 400);
  return promise;
}

function createNote(client, note) { // eslint-disable-line no-unused-vars
  return client
    .collection('notes', { idSchema: notesIdSchema })
    .create(note, { useRecordId: true });
}

function deleteNote(client, id) { // eslint-disable-line no-unused-vars
  return client.collection('notes', { idSchema: notesIdSchema }).deleteAny(id);
}

function disconnectFromKinto(client) { // eslint-disable-line no-unused-vars
  const notes = client.collection('notes', { idSchema: notesIdSchema });
  return notes.resetSyncStatus();
}

<|endoftext|>


<commit_before>import path from 'path'
import should from 'should'
import appRoot from 'app-root-path'
import pg from 'pg.js'

import pgUtil from './util'

const q = "SELECT 'John Doe'::text AS name"
const qParam = "SELECT $1::text AS name"
const param = 'John Doe'

describe('pg-util', function() {
  before(function() {
    const pathToSQLFiles = path.join(appRoot.path, '/resources/tests/sql')
    const db = pgUtil(null, pathToSQLFiles)
    this.db = db
  })

  describe('#getClient', function() {
    it('should get a client', async function() {
      const client = await this.db.getClient()

      should.exist(client)
      should.exist(client.query)
      client.query.should.be.a.Function()
    })
  })

  describe('#query', function() {
    it('should perform a query', async function() {
      const rows = await this.db.query(q)
      should.equal(rows[0].name, param)
    })

    it('should perform a parameterized query', async function() {
      const rows = await this.db.query(qParam, [param])
      should.equal(rows[0].name, param)
    })

    it('should perform a query with a specified client', async function() {
      const client = await this.db.getClient()
      const rows = await this.db.query(client, q)
      client.done()
      should.equal(rows[0].name, param)
    })

    it('should perform a parameterized query with a specified client', async function() {
      const client = await this.db.getClient()
      const rows = await this.db.query(client, qParam, [param])
      client.done()
      should.equal(rows[0].name, param)
    })
  })

  describe('#one', function() {
    it('should run a query returning the first row', async function() {
      const row = await this.db.one(q)
      should.equal(row.name, param)
    })

    it('should run a query returning the first row (parameterized)', async function() {
      const row = await this.db.one(qParam, [param])
      should.equal(row.name, param)
    })

    it('should run a query returning the first row with a specified client', async function() {
      const client = await this.db.getClient()
      const row = await this.db.one(client, q)
      client.done()
      should.equal(row.name, param)
    })

    it('should run a query returning the first row with a specified client (parameterized)', async function() {
      const client = await this.db.getClient()
      const row = await this.db.one(client, qParam, [param])
      client.done()
      should.equal(row.name, param)
    })
  })

  describe('#run', function() {
    it('should run a query using a given sql file', async function() {
      const rows = await this.db.run('select')
      should.equal(rows[0].name, param)
    })

    it('should run a query using a given sql file (parameterized)', async function() {
      const rows = await this.db.run('select-param', [param])
      should.equal(rows[0].name, param)
    })

    it('should run a query using a given sql file and client', async function() {
      const client = await this.db.getClient()
      const rows = await this.db.run(client, 'select')
      client.done()
      should.equal(rows[0].name, param)
    })

    it('should run a query using a given sql file and client (parameterized)', async function() {
      const client = await this.db.getClient()
      const rows = await this.db.run(client, 'select-param', [param])
      client.done()
      should.equal(rows[0].name, param)
    })
  })

  describe('#first', function() {
    it('should run a query returning the first row using a given sql file', async function() {
      const row = await this.db.first('select')
      should.equal(row.name, param)
    })

    it('should run a query returning the first row using a given sql file (parameterized)', async function() {
      const row = await this.db.first('select-param', [param])
      should.equal(row.name, param)
    })

    it('should run a query returning the first row using a given sql file and client', async function() {
      const client = await this.db.getClient()
      const row = await this.db.first(client, 'select')
      client.done()
      should.equal(row.name, param)
    })

    it('should run a query returning the first row using a given sql file and client (parameterized)', async function() {
      const client = await this.db.getClient()
      const row = await this.db.first(client, 'select-param', [param])
      client.done()
      should.equal(row.name, param)
    })
  })

  describe('transactions', function() {
    it('should re-use the same client', async function() {
      const createTableSQL = `CREATE TABLE boo (
        name TEXT NOT NULL,
        email TEXT NOT NULL PRIMARY KEY
      );`
      const insertSQL = `INSERT INTO boo(name, email) VALUES ('John Doe', 'test@test.com');`
      const selectSQL = 'SELECT * FROM boo;'

      await this.db.transaction(async (tx) => {
        await tx.query(createTableSQL)
        await tx.query(insertSQL)

        let row = null

        // should return a row when using the client used in the transaction
        row = await tx.one(selectSQL)
        should.equal(row.name, 'John Doe')
        should.equal(row.email, 'test@test.com')

        // should error with a client not used for the transaction
        try {
          row = await this.db.one(selectSQL)
          should.not.exist(row)
        } catch (error) {
          should.exist(error)
          should.equal(error.code, '42P01')
        }

        await tx.query('ABORT')
      })
    })
  })

  describe('transactions without helper', function() {
    it('should re-use the same client', async function() {
      const createTableSQL = `CREATE TABLE boo (
        name TEXT NOT NULL,
        email TEXT NOT NULL PRIMARY KEY
      );`
      const insertSQL = `INSERT INTO boo(name, email) VALUES ('John Doe', 'test@test.com');`
      const selectSQL = 'SELECT * FROM boo;'

      const client = await this.db.getClient()

      await this.db.query(client, 'BEGIN')
      await this.db.query(client, createTableSQL)
      await this.db.query(client, insertSQL)

      let row = null

      // should return a row when using the client used in the transaction
      row = await this.db.one(client, selectSQL)
      should.equal(row.name, 'John Doe')
      should.equal(row.email, 'test@test.com')

      // should error with a client not used for the transaction
      try {
        row = await this.db.one(selectSQL)
        should.not.exist(row)
      } catch (error) {
        should.exist(error)
        should.equal(error.code, '42P01')
      }

      await this.db.query(client, 'ABORT')
      client.done()
    })
  })
})
<commit_msg>improved tests<commit_after>import path from 'path'
import should from 'should'
import appRoot from 'app-root-path'
import pg from 'pg.js'

import pgUtil from './util'

const q = "SELECT 'John Doe'::text AS name"
const qParam = "SELECT $1::text AS name"
const param = 'John Doe'

describe('pg-util', function() {
  before(function() {
    const pathToSQLFiles = path.join(appRoot.path, '/resources/tests/sql')
    const db = pgUtil(null, pathToSQLFiles)
    this.db = db
  })

  describe('#getClient', function() {
    it('should get a client', async function() {
      const client = await this.db.getClient()

      should.exist(client)
      should.exist(client.query)
      client.query.should.be.a.Function()
    })
  })

  describe('#query', function() {
    it('should perform a query', async function() {
      const rows = await this.db.query(q)
      should.equal(rows[0].name, param)
    })

    it('should perform a parameterized query', async function() {
      const rows = await this.db.query(qParam, [param])
      should.equal(rows[0].name, param)
    })

    it('should perform a query with a specified client', async function() {
      const client = await this.db.getClient()
      const rows = await this.db.query(client, q)
      client.done()
      should.equal(rows[0].name, param)
    })

    it('should perform a parameterized query with a specified client', async function() {
      const client = await this.db.getClient()
      const rows = await this.db.query(client, qParam, [param])
      client.done()
      should.equal(rows[0].name, param)
    })
  })

  describe('#one', function() {
    it('should run a query returning the first row', async function() {
      const row = await this.db.one(q)
      should.equal(row.name, param)
    })

    it('should run a query returning the first row (parameterized)', async function() {
      const row = await this.db.one(qParam, [param])
      should.equal(row.name, param)
    })

    it('should run a query returning the first row with a specified client', async function() {
      const client = await this.db.getClient()
      const row = await this.db.one(client, q)
      client.done()
      should.equal(row.name, param)
    })

    it('should run a query returning the first row with a specified client (parameterized)', async function() {
      const client = await this.db.getClient()
      const row = await this.db.one(client, qParam, [param])
      client.done()
      should.equal(row.name, param)
    })
  })

  describe('#run', function() {
    it('should run a query using a given sql file', async function() {
      const rows = await this.db.run('select')
      should.equal(rows[0].name, param)
    })

    it('should run a query using a given sql file (parameterized)', async function() {
      const rows = await this.db.run('select-param', [param])
      should.equal(rows[0].name, param)
    })

    it('should run a query using a given sql file and client', async function() {
      const client = await this.db.getClient()
      const rows = await this.db.run(client, 'select')
      client.done()
      should.equal(rows[0].name, param)
    })

    it('should run a query using a given sql file and client (parameterized)', async function() {
      const client = await this.db.getClient()
      const rows = await this.db.run(client, 'select-param', [param])
      client.done()
      should.equal(rows[0].name, param)
    })
  })

  describe('#first', function() {
    it('should run a query returning the first row using a given sql file', async function() {
      const row = await this.db.first('select')
      should.equal(row.name, param)
    })

    it('should run a query returning the first row using a given sql file (parameterized)', async function() {
      const row = await this.db.first('select-param', [param])
      should.equal(row.name, param)
    })

    it('should run a query returning the first row using a given sql file and client', async function() {
      const client = await this.db.getClient()
      const row = await this.db.first(client, 'select')
      client.done()
      should.equal(row.name, param)
    })

    it('should run a query returning the first row using a given sql file and client (parameterized)', async function() {
      const client = await this.db.getClient()
      const row = await this.db.first(client, 'select-param', [param])
      client.done()
      should.equal(row.name, param)
    })
  })

  describe('transactions', function() {
    it('should work with #query', async function() {
      await this.db.transaction(async (tx) => {
        const rows = await tx.query(q)
        should.equal(rows[0].name, param)
      })
    })

    it('should work with #one', async function() {
      await this.db.transaction(async (tx) => {
        const row = await tx.one(q)
        should.equal(row.name, param)
      })
    })

    it('should work with #run', async function() {
      await this.db.transaction(async (tx) => {
        const rows = await tx.run('select')
        should.equal(rows[0].name, param)
      })
    })

    it('should work with #first', async function() {
      await this.db.transaction(async (tx) => {
        const row = await tx.first('select')
        should.equal(row.name, param)
      })
    })

    it('should re-use the same client for multiple queries', async function() {
      const createTableSQL = `CREATE TABLE boo (
        name TEXT NOT NULL,
        email TEXT NOT NULL PRIMARY KEY
      );`
      const insertSQL = `INSERT INTO boo(name, email) VALUES ('John Doe', 'test@test.com');`
      const selectSQL = 'SELECT * FROM boo;'

      await this.db.transaction(async (tx) => {
        // test #query
        await tx.query(createTableSQL)
        await tx.query(insertSQL)

        let row = null

        // should return a row when using the client used in the transaction
        // test #one
        row = await tx.one(selectSQL)
        should.equal(row.name, 'John Doe')
        should.equal(row.email, 'test@test.com')

        // should error with a client not used for the transaction
        try {
          row = await this.db.one(selectSQL)
          should.not.exist(row)
        } catch (error) {
          should.exist(error)
          should.equal(error.code, '42P01')
        }

        await tx.query('ABORT')
      })
    })
  })

  describe('transactions without the #transaction helper', function() {
    it('should not allow clients outside of a transaction to see non-commited values', async function() {
      const createTableSQL = `CREATE TABLE boo (
        name TEXT NOT NULL,
        email TEXT NOT NULL PRIMARY KEY
      );`
      const insertSQL = `INSERT INTO boo(name, email) VALUES ('John Doe', 'test@test.com');`
      const selectSQL = 'SELECT * FROM boo;'

      const client = await this.db.getClient()

      await this.db.query(client, 'BEGIN')
      await this.db.query(client, createTableSQL)
      await this.db.query(client, insertSQL)

      let row = null

      // should return a row when using the client used in the transaction
      row = await this.db.one(client, selectSQL)
      should.equal(row.name, 'John Doe')
      should.equal(row.email, 'test@test.com')

      // should error with a client not used for the transaction
      try {
        row = await this.db.one(selectSQL)
        should.not.exist(row)
      } catch (error) {
        should.exist(error)
        should.equal(error.code, '42P01')
      }

      await this.db.query(client, 'ABORT')
      client.done()
    })
  })
})
<|endoftext|>


<commit_before>applicantControllers.controller('ApplicantCtrl', ['$scope', '$location', 'Applicant', 'Label', 'Suggest', 'Upload', '$window', 'ngToast', '$timeout',
  function($scope, $location, Applicant, Label, Suggest, Upload, $window, ngToast, $timeout) {

    //default dropdown menu to 'new' on page load
    $scope.selection = "new";
    $scope.sort = "score";
    $scope.sortOrder = "desc";

    //query should start off at index 0, displaying first item
    $scope.index = 0;
    //displaying 25 applicants at a time
    $scope.pageSize = 25;
    $scope.loadingData = false;
    $scope.hasData = true;

    //sorting table by column code
    $scope.propertyName = null;
    $scope.reverse = false;
    $scope.searchText = "";
    $scope.displayText = "";

    /**
     * function that changes the suggestion query based on the text that is in the search bar
     *
     * @param text- text that is currently in the search bar
     */

    $scope.autoComplete = function(text) {
      $scope.searchText = text;
      $scope.autoSuggest = Suggest.query({
        term: $scope.searchText
      });
    }

    //alternative to ng-change, calls $scope.autoComplete when 'searchText' has been modified
    $scope.$watch('displayText', $scope.autoComplete);

    //default query
    $scope.applicants = Applicant.query({
      from: $scope.index,
      size: $scope.pageSize,
      sort: $scope.sort,
      order: $scope.sortOrder
    });

    /**
     * sort by property name. function is called when column is clicked
     *
     * @param type- type to sort by (i.e. Score)
     */

    $scope.sortColumn = function(type) {
      $scope.index = 0;
      $scope.hasData = true;
      $scope.sort = type;

      if (type == "score") {
        $scope.sortBool = false;
      } else {
        $scope.sortBool = true;
      }

      if ($scope.sortOrder == "asc") {
        $scope.sortOrder = "desc";
        $scope.reverse = false;
      }

      else if ($scope.sortOrder == "desc") {
        $scope.sortOrder = "asc";
        $scope.reverse = true;
      }

      $scope.applicants = Applicant.query({
        type: $scope.selection,
        from: $scope.index,
        size: $scope.pageSize,
        sort: $scope.sort,
        order: $scope.sortOrder,
        query: $scope.searchText
      });
    }

    /**
     * change queries when new type is selected from the dropdown menu
     *
     * @param type- select box value
     */
    $scope.showSelectValue = function(type) {
      $scope.searchText = "";
      $scope.index = 0;
      $scope.hasData = true;
      $scope.selection = type;
      $scope.applicants = Applicant.query({
        type: type,
        from: $scope.index,
        size: $scope.pageSize,
        sort: $scope.sort,
        order: $scope.sortOrder
      });
    };

    /**
     * adds to query if there is more data, else change hasData to false
     *
     * @param result- rest of the query
     */
    $scope.dataLoaded = function(result) {
      if (result.length > 0) {
        $scope.applicants = $scope.applicants.concat(result);
      } else {
        $scope.hasData = false;
        $scope.index = 0;
      }

      $scope.loadingData = false;
    };

    /**
     * check if there is more data to load from the query, for infinite scroll
     *
     */
    $scope.nextPage = function() {
      if ($scope.hasData) {
        $scope.loadingData = true;
        $scope.index += $scope.pageSize;

        Applicant.query({
          query: $scope.searchText,
          type: $scope.selection,
          from: $scope.index,
          size: $scope.pageSize,
          order: $scope.sortOrder,
          sort: $scope.sort
        }, $scope.dataLoaded);
      };
    }

    // Only enable if the document has a long scroll bar
    // Note the window height + offset
    $('#top-link-block').removeClass('hidden').affix({
      offset: {
        top: 100
      }
    });

    /**
     * function that is called when action button is clicked, i.e. Favorite, Archive, Review
     *
     * @param id- id number of the applicant
     * @param type- type of the applicant
     * @param applicant- applicant object itself, passed in to avoid wrong indexing
     */
    $scope.mark = function(id, type, applicant) {
      var label = new Label({
        'id': id,
        'type': type
      });
      label.$save().then(function() {
        $scope.applicants.splice($scope.applicants.indexOf(applicant), 1);
      });
    }

    /**
     * function that is called when applicant is placed back in 'New
     *
     * @param id- id number of the applicant
     * @param applicant- applicant object itself, passed in to avoid wrong indexing
     */
    $scope.remove = function(id, applicant) {
      Label.delete({
        'id': id
      }).$promise.then(function() {
        $scope.applicants.splice($scope.applicants.indexOf(applicant), 1);
      });
    }

    /**
     * change toast CSS to show the message
     * after about three seconds, hide the toast
     *
     * @param type- type of toast to show
     *
     */
    $scope.showToast = function(type) {
      if (type == 'Favorite') {
        ngToast.create("Applicant added to Favorites");
      }

      else if (type == 'Review') {
        ngToast.create({
          className: 'warning',
          content: 'Applicant added to Review'
        });

      }

      else if (type == 'Archive') {
        ngToast.create({
          className: 'danger',
          content: 'Applicant added to Archive'
        });

      }

      else if(type == 'Upload'){
        ngToast.create("Resume has been Uploaded");
      }

      else {
        ngToast.create({
          className: 'info',
          content: 'Applicant sent back to home page'
        });

      }
    }

    /**
     * return document (image or PDF) from a link
     *
     * @param id- id of applicant
     * @param type- type of document either an image or a PDF
     *
     */

    $scope.getLink = function(id, type) {
      return "service/attachments?id=" + id + "&type=" + type;
    }

    /**
     * return query based on text that was input in search bar
     *
     * @param searchText- text that was input in search bar
     *
     */
    $scope.search = function(searchText) {
      $scope.index = 0;
      $scope.searchText = searchText;

      //major advanced search
      var majString = "";
      var csChecked = document.getElementById("csCheck").checked;
      var cpeChecked = document.getElementById("cpeCheck").checked;
      var itChecked = document.getElementById("itCheck").checked;
      if (csChecked) {
        majString = majString + "'computer science'";
      }

      if (cpeChecked) {
        majString = majString + "'computer engineering'";
      }

      if (itChecked) {
        majString = majString + "'information technology'";
      }

      if (majString != "") {

        $scope.searchText = $scope.searchText + " AND education.degree: (" + majString + ")";
      }

      //location advanced search
      var locString = "";
      var vaChecked = document.getElementById("vaCheck").checked;
      var mdChecked = document.getElementById("mdCheck").checked;
      var dcChecked = document.getElementById("dcCheck").checked;

      if (vaChecked) {
        locString = locString + "'VA'";
      }

      if (mdChecked) {
        locString = locString + "'MD'";
      }

      if (dcChecked) {
        locString = locString + "'DC'";
      }

      if (locString != "") {
        $scope.searchText = $scope.searchText + " AND currentLocation.location: (" + locString + ")";
      }

      //education advanced search
      var colString = "";
      var uvaChecked = document.getElementById("uvaCheck").checked;
      var jmuChecked = document.getElementById("jmuCheck").checked;
      var rpiChecked = document.getElementById("rpiCheck").checked;

      if (uvaChecked) {
        colString = colString + "'Virginia'";
      }

      if (jmuChecked) {
        colString = colString + "'James Madison'";
      }

      if (rpiChecked) {
        colString = colString + "'Rensselaer'";
      }

      if (colString != "") {
        $scope.searchText = $scope.searchText + " AND education.school: (" + colString + ")";
      }

      //job title advanced search
      var jobString = "";
      var devChecked = document.getElementById("devCheck").checked;
      var arcChecked = document.getElementById("arcCheck").checked;
      var manChecked = document.getElementById("manCheck").checked;

      if (devChecked) {
        jobString = jobString + "'developer'";
      }

      if (arcChecked) {
        jobString = jobString + "'architect'";
      }

      if (manChecked) {
        jobString = jobString + "'manager'";
      }

      if (jobString != "") {
        $scope.searchText = $scope.searchText + " AND currentLocation.title: (" + jobString + ")";
      }

      $scope.applicants = Applicant.query({
        query: $scope.searchText,
        from: $scope.index,
        size: $scope.pageSize,
        sort: $scope.sort,
        order: $scope.sortOrder
      });

      $scope.displayText = searchText;
    }

    /**
     * Converts user uploaded files to base64 strings and indexes them
     *
     */
    $scope.upload = function() {
      var files = document.querySelector('input[type=file]').files;
      for(var i = 0; i < files.length; i++) {
        (function(file) {
          if(file.type === 'application/pdf' || file.type === 'application/doc') {
            var reader = new FileReader();

            reader.addEventListener("load", function () {
              var temp = reader.result;
              var base64string = temp.substring(28);
              console.log(base64string);
              console.log(file.type);

              var upload = new Upload({
                'type': 'upload',
                'base64string': base64string
              });

              upload.$save();
            }, false);

            if(file)
              reader.readAsDataURL(file);
          } else
              alert('only pdf or word doc files')
        })(files[i]);
      }
    }

    //scroll code
    $(function() {
      var lastScrollTop = 0,
        delta = 5;
      $(window).scroll(function(event) {
        var st = $(this).scrollTop();

        if (Math.abs(lastScrollTop - st) <= delta)
          return;

        //if scrolling up, hide the footer
        if (st <= lastScrollTop) {;
          angular.element("#footer").hide();
        }

        lastScrollTop = st;
      });
    });

    //if at bottom of window, show footer
    $(window).scroll(function() {
      if ($(window).scrollTop() + $(window).height() == $(document).height()) {
        angular.element("#footer").show();
      } else {
        angular.element("#footer").hide();
      }
    });

  }]);

/**
*
* A custom directive to bind file upload
*
* @param: The attributes name
* @param: A callback function which binds the upload function to the attribute
*/
applicantControllers.directive('customOnChange', function() {
  return {
    restrict: 'A',
    link: function (scope, element, attrs) {
      var onChangeFunc = scope.$eval(attrs.customOnChange);
      element.bind('change', onChangeFunc);
    }
  };
})
<commit_msg>fixed search bar display text<commit_after>applicantControllers.controller('ApplicantCtrl', ['$scope', '$location', 'Applicant', 'Label', 'Suggest', 'Upload', '$window', 'ngToast', '$timeout',
  function($scope, $location, Applicant, Label, Suggest, Upload, $window, ngToast, $timeout) {

    //default dropdown menu to 'new' on page load
    $scope.selection = "new";
    $scope.sort = "score";
    $scope.sortOrder = "desc";

    //query should start off at index 0, displaying first item
    $scope.index = 0;
    //displaying 25 applicants at a time
    $scope.pageSize = 25;
    $scope.loadingData = false;
    $scope.hasData = true;

    //sorting table by column code
    $scope.propertyName = null;
    $scope.reverse = false;
    $scope.searchText = "";
    $scope.displayText = "";

    /**
     * function that changes the suggestion query based on the text that is in the search bar
     *
     * @param text- text that is currently in the search bar
     */

    $scope.autoComplete = function(text) {
      $scope.searchText = text;
      $scope.autoSuggest = Suggest.query({
        term: $scope.searchText
      });
    }

    //alternative to ng-change, calls $scope.autoComplete when 'searchText' has been modified
    $scope.$watch('displayText', $scope.autoComplete);

    //default query
    $scope.applicants = Applicant.query({
      from: $scope.index,
      size: $scope.pageSize,
      sort: $scope.sort,
      order: $scope.sortOrder
    });

    /**
     * sort by property name. function is called when column is clicked
     *
     * @param type- type to sort by (i.e. Score)
     */

    $scope.sortColumn = function(type) {
      $scope.index = 0;
      $scope.hasData = true;
      $scope.sort = type;

      if (type == "score") {
        $scope.sortBool = false;
      } else {
        $scope.sortBool = true;
      }

      if ($scope.sortOrder == "asc") {
        $scope.sortOrder = "desc";
        $scope.reverse = false;
      }

      else if ($scope.sortOrder == "desc") {
        $scope.sortOrder = "asc";
        $scope.reverse = true;
      }

      $scope.applicants = Applicant.query({
        type: $scope.selection,
        from: $scope.index,
        size: $scope.pageSize,
        sort: $scope.sort,
        order: $scope.sortOrder,
        query: $scope.searchText
      });
    }

    /**
     * change queries when new type is selected from the dropdown menu
     *
     * @param type- select box value
     */
    $scope.showSelectValue = function(type) {
      $scope.searchText = "";
      $scope.displayText = "";
      $scope.index = 0;
      $scope.hasData = true;
      $scope.selection = type;
      $scope.applicants = Applicant.query({
        type: type,
        from: $scope.index,
        size: $scope.pageSize,
        sort: $scope.sort,
        order: $scope.sortOrder
      });
    };

    /**
     * adds to query if there is more data, else change hasData to false
     *
     * @param result- rest of the query
     */
    $scope.dataLoaded = function(result) {
      if (result.length > 0) {
        $scope.applicants = $scope.applicants.concat(result);
      } else {
        $scope.hasData = false;
        $scope.index = 0;
      }

      $scope.loadingData = false;
    };

    /**
     * check if there is more data to load from the query, for infinite scroll
     *
     */
    $scope.nextPage = function() {
      if ($scope.hasData) {
        $scope.loadingData = true;
        $scope.index += $scope.pageSize;

        Applicant.query({
          query: $scope.searchText,
          type: $scope.selection,
          from: $scope.index,
          size: $scope.pageSize,
          order: $scope.sortOrder,
          sort: $scope.sort
        }, $scope.dataLoaded);
      };
    }

    // Only enable if the document has a long scroll bar
    // Note the window height + offset
    $('#top-link-block').removeClass('hidden').affix({
      offset: {
        top: 100
      }
    });

    /**
     * function that is called when action button is clicked, i.e. Favorite, Archive, Review
     *
     * @param id- id number of the applicant
     * @param type- type of the applicant
     * @param applicant- applicant object itself, passed in to avoid wrong indexing
     */
    $scope.mark = function(id, type, applicant) {
      var label = new Label({
        'id': id,
        'type': type
      });
      label.$save().then(function() {
        $scope.applicants.splice($scope.applicants.indexOf(applicant), 1);
      });
    }

    /**
     * function that is called when applicant is placed back in 'New
     *
     * @param id- id number of the applicant
     * @param applicant- applicant object itself, passed in to avoid wrong indexing
     */
    $scope.remove = function(id, applicant) {
      Label.delete({
        'id': id
      }).$promise.then(function() {
        $scope.applicants.splice($scope.applicants.indexOf(applicant), 1);
      });
    }

    /**
     * change toast CSS to show the message
     * after about three seconds, hide the toast
     *
     * @param type- type of toast to show
     *
     */
    $scope.showToast = function(type) {
      if (type == 'Favorite') {
        ngToast.create("Applicant added to Favorites");
      }

      else if (type == 'Review') {
        ngToast.create({
          className: 'warning',
          content: 'Applicant added to Review'
        });

      }

      else if (type == 'Archive') {
        ngToast.create({
          className: 'danger',
          content: 'Applicant added to Archive'
        });

      }

      else if(type == 'Upload'){
        ngToast.create("Resume has been Uploaded");
      }

      else {
        ngToast.create({
          className: 'info',
          content: 'Applicant sent back to home page'
        });

      }
    }

    /**
     * return document (image or PDF) from a link
     *
     * @param id- id of applicant
     * @param type- type of document either an image or a PDF
     *
     */

    $scope.getLink = function(id, type) {
      return "service/attachments?id=" + id + "&type=" + type;
    }

    /**
     * return query based on text that was input in search bar
     *
     * @param searchText- text that was input in search bar
     *
     */
    $scope.search = function(searchText) {
      $scope.index = 0;
      $scope.searchText = searchText;

      //major advanced search
      var majString = "";
      var csChecked = document.getElementById("csCheck").checked;
      var cpeChecked = document.getElementById("cpeCheck").checked;
      var itChecked = document.getElementById("itCheck").checked;
      if (csChecked) {
        majString = majString + "'computer science'";
      }

      if (cpeChecked) {
        majString = majString + "'computer engineering'";
      }

      if (itChecked) {
        majString = majString + "'information technology'";
      }

      if (majString != "") {

        $scope.searchText = $scope.searchText + " AND education.degree: (" + majString + ")";
      }

      //location advanced search
      var locString = "";
      var vaChecked = document.getElementById("vaCheck").checked;
      var mdChecked = document.getElementById("mdCheck").checked;
      var dcChecked = document.getElementById("dcCheck").checked;

      if (vaChecked) {
        locString = locString + "'VA'";
      }

      if (mdChecked) {
        locString = locString + "'MD'";
      }

      if (dcChecked) {
        locString = locString + "'DC'";
      }

      if (locString != "") {
        $scope.searchText = $scope.searchText + " AND currentLocation.location: (" + locString + ")";
      }

      //education advanced search
      var colString = "";
      var uvaChecked = document.getElementById("uvaCheck").checked;
      var jmuChecked = document.getElementById("jmuCheck").checked;
      var rpiChecked = document.getElementById("rpiCheck").checked;

      if (uvaChecked) {
        colString = colString + "'Virginia'";
      }

      if (jmuChecked) {
        colString = colString + "'James Madison'";
      }

      if (rpiChecked) {
        colString = colString + "'Rensselaer'";
      }

      if (colString != "") {
        $scope.searchText = $scope.searchText + " AND education.school: (" + colString + ")";
      }

      //job title advanced search
      var jobString = "";
      var devChecked = document.getElementById("devCheck").checked;
      var arcChecked = document.getElementById("arcCheck").checked;
      var manChecked = document.getElementById("manCheck").checked;

      if (devChecked) {
        jobString = jobString + "'developer'";
      }

      if (arcChecked) {
        jobString = jobString + "'architect'";
      }

      if (manChecked) {
        jobString = jobString + "'manager'";
      }

      if (jobString != "") {
        $scope.searchText = $scope.searchText + " AND currentLocation.title: (" + jobString + ")";
      }

      $scope.applicants = Applicant.query({
        query: $scope.searchText,
        from: $scope.index,
        size: $scope.pageSize,
        sort: $scope.sort,
        order: $scope.sortOrder
      });

      $scope.displayText = searchText;
    }

    /**
     * Converts user uploaded files to base64 strings and indexes them
     *
     */
    $scope.upload = function() {
      var files = document.querySelector('input[type=file]').files;
      for(var i = 0; i < files.length; i++) {
        (function(file) {
          if(file.type === 'application/pdf' || file.type === 'application/doc') {
            var reader = new FileReader();

            reader.addEventListener("load", function () {
              var temp = reader.result;
              var base64string = temp.substring(28);
              console.log(base64string);
              console.log(file.type);

              var upload = new Upload({
                'type': 'upload',
                'base64string': base64string
              });

              upload.$save();
            }, false);

            if(file)
              reader.readAsDataURL(file);
          } else
              alert('only pdf or word doc files')
        })(files[i]);
      }
    }

    //scroll code
    $(function() {
      var lastScrollTop = 0,
        delta = 5;
      $(window).scroll(function(event) {
        var st = $(this).scrollTop();

        if (Math.abs(lastScrollTop - st) <= delta)
          return;

        //if scrolling up, hide the footer
        if (st <= lastScrollTop) {;
          angular.element("#footer").hide();
        }

        lastScrollTop = st;
      });
    });

    //if at bottom of window, show footer
    $(window).scroll(function() {
      if ($(window).scrollTop() + $(window).height() == $(document).height()) {
        angular.element("#footer").show();
      } else {
        angular.element("#footer").hide();
      }
    });

  }]);

/**
*
* A custom directive to bind file upload
*
* @param: The attributes name
* @param: A callback function which binds the upload function to the attribute
*/
applicantControllers.directive('customOnChange', function() {
  return {
    restrict: 'A',
    link: function (scope, element, attrs) {
      var onChangeFunc = scope.$eval(attrs.customOnChange);
      element.bind('change', onChangeFunc);
    }
  };
})
<|endoftext|>


<commit_before>
(function() {

// some API urls
var google = "http://ajax.googleapis.com/ajax/services/search/web?v=1.0&callback={callback}&rsz=large&q={query}",
    questions = "http://api.stackoverflow.com/1.0/questions/{id}?key={key}&jsonp={callback}",
    questionsTagged = "http://api.stackoverflow.com/1.0/questions/?key={key}&tagged={tagged}&jsonp={callback}",
    search = "http://api.stackoverflow.com/1.0/search/?key={key}&intitle={intitle}&nottagged={nottagged}&tagged={tagged}&jsonp={callback}",
    unansweredQuestionsTagged = "http://api.stackoverflow.com/1.0/questions/unanswered/?key={key}&tagged={tagged}&jsonp={callback}",
    questionsByUser = "http://api.stackoverflow.com/1.0/users/{id}/questions?key={key}&jsonp={callback}",
    // prevent loading more than once
    isLoaded,
    // each call creates a unique jsonp callback
    jsonpCount = 0,
    // for matching the question id inside a stackexchange question url
    regexQuestion = /\/questions\/([0-9]*)\//ig,
    // those listening for the loaded event
    callbacks = [];

function domLoaded() {
    // based on the proven method of simulating DOMContentLoaded in IE
    if (window.addEventListener) {
        window.addEventListener("load", loaded, false);
    }
    else {
        window.attachEvent("onload", loaded);
    }
    var check;
    if (window.attachEvent) {
        if ((window == window.top) && document.documentElement.doScroll) {
            var timeout, er, el = document.createElement("div");
            check = function() {
                try {
                    el.doScroll("left");
                }
                catch (er) {
                    timeout = window.setTimeout(check, 0);
                    return;
                }
                el = null;
                loaded();
            }
            check();
        }
        else {
            document.onreadystatechange = function() {
                if (/loaded|complete/.test(document.readyState)) {
                    loaded();
                }
            };
        }
    }
    else if (document.addEventListener) {
        document.addEventListener("DOMContentLoaded", loaded, false);
    }
}

function loaded() {
    // called when the DOM is ready
    if (!isLoaded) {
        isLoaded = true;
        for (var i = 0, l = callbacks.length; i < l; i++) {
            callbacks[i]();
        }
    }
}

function jsonp(url, params, callback) {
    // begin a jsonp request and foward the response
    var jsonp = "_jsonp" + jsonpCount++;
    url = url
        .replace("{callback}", "stackunderflow." + jsonp)
        .replace("{key}", su.appId);
    if (params) {
        for (var name in params) {
            url = url.replace("{" + name + "}", params[name]);
        }
    }
    su[jsonp] = function(data) {
        delete su[jsonp];
        su.loaded(function() {
            callback(data);
        });
    };
    var scr = document.createElement("script");
    scr.type = "text/javascript";
    scr.src = url;
    // insertBefore in case this is performed inline in the head
    var head = document.getElementsByTagName("head")[0];
    head.insertBefore(scr, head.firstChild);
}

function getUniqueQuestions(searchResults) {
    // created an array of unique question ids
    // from a set of google search results
    var questions = [], index = {};
    for (var i = 0, l = searchResults.length; i < l; i++) {
        regexQuestion.lastIndex = 0;
        var url = searchResults[i].unescapedUrl,
            match = regexQuestion.exec(url);
        if ( match && match[1] ) {
            var questionId = parseInt(match[1]);
            if (!index[questionId]) {
                questions.push(questionId);
                index[questionId] = 1;
            }
        }
    }
    return questions;
}

function pad(num) {
    var s = num+"";
    return s.length === 2 ? s : ("0" + s);
}
function applyTemplate(html, obj) {
    var i, l, regexTokens = new RegExp("\\{([^}]*)\\}", "g"), match, token,
        tokenValues = {
            site: su.site
        };
    do {
        match = regexTokens.exec(html);
        token = match ? match[1] : null;
        if (token) {
            var index = token.indexOf(":"),
                filter = null;
            if (index > -1) {
                filter = token.substr(0, index);
                token = token.substr(index + 1);
            }
            var parts = token === "=" ? [] : token.split('.'),
                step = obj;
            for (var i = 0, l = parts.length; i < l; i++) {
                step = step[parts[i]];
                if (typeof step === "undefined") break;
            }
            if (filter) {
                var template;
                if (filter.indexOf("template-") > -1) {
                    template = filter.substr("template-".length);
                    filter = "template";
                }
                filter = su.filters[filter];
                if (filter) {
                    step = filter(step, template);
                }
            }
            if (typeof step !== "undefined") {
                tokenValues[match[1]] = step;
            }
        }
    }
    while (token);
    for (token in tokenValues) {
        html = html.replace(new RegExp("\\{" + token.replace(/([\.\:\-])/g, '\\$1') + "\\}","g"), tokenValues[token]);
    }
    return html;
}

function append(target, html) {
    var div = document.createElement("div");
    div.innerHTML = html;
    while(div.firstChild) {
        target.appendChild(div.firstChild);
    }
}

function execQuestions(url, params, complete) {
    var renderArgs,
        ctx = { render: function() { renderArgs = arguments; } };
    jsonp(url, params, function(questions) {
        if (complete) complete(questions);
        if (renderArgs) {
            renderArgs = Array.prototype.slice.apply(renderArgs);
            renderArgs.splice(0, 0, questions);
            su.render.questions.apply(null, renderArgs);
        }
    });
    return ctx;
}

var su = window.stackunderflow = {
    appId: "oxXcnoD51kKE-crj7TadaA",
    site: "http://stackoverflow.com",
    loaded: function(callback) {
        if (isLoaded) {
            callback();
        }
        else {
            callbacks.push(callback);
        }
    },
    googleQuestions: function(term, complete) {
        var renderArgs,
            ctx = { render: function() { renderArgs = arguments; } };
        var site = su.site + "/questions";
        term = term || ('"' + window.location + '"');
        jsonp(google, {
            query: "site:" + site + " " + term
        },
        function(data) {
            var questions = getUniqueQuestions(data.responseData.results);
            if (questions.length) {
                su.getQuestions(questions, function(questions) {
                    if (complete) complete(questions);
                    if (renderArgs) {
                        renderArgs = Array.prototype.slice.apply(renderArgs);
                        renderArgs.splice(0, 0, questions);
                        su.render.questions.apply(null, renderArgs);
                    }
                });
            }
        });
        return ctx;
    },
    getQuestions: function(questionIds, complete) {
        return execQuestions(questions, { id: questionIds.join(';') }, complete);
    },
    searchQuestions: function(intitle, tagged, notTagged, complete) {
        return execQuestions(search, { tagged: tagged, nottagged: notTagged, intitle: intitle }, complete);
    },
    getQuestionsWithTags: function(tags, onlyUnanswered, complete) {
        return execQuestions(onlyUnanswered ? unansweredQuestionsTagged : questionsTagged,
            { tagged: tags }, complete);
    },
    getQuestionsByUser: function(userIds, complete) {
        return execQuestions(questionsByUser, { id: userIds instanceof Array ? userIds.join(';') : userIds }, complete);
    },
    render: {
        questions: function(questions, target, template) {
            template = template || "question";
            if (typeof target === "string") {
                if (target.charAt(0) === "#") target = target.substr(1);
                target = document.getElementById(target);
            }
            var html = "";
            questions = questions.questions;
            if (questions) {
                for (var i = 0, l = questions.length; i < l; i++) {
                    html += applyTemplate(su.templates[template], questions[i]);
                }
            }
            append(target, html);
        }
    },
    templates: {
        tag: '{=} ',
        question: '
 \

 \

 \

{up_vote_count}
 \

votes
 \

 \

 \

{answer_count}
\

answer
 \

 \

 \

{viewcountnumber:view_count}
 \

{viewcountk:view_count}views
 \

 \

 \

 \

{title}
 \

 \
            {template-tag:tags}\

 \

 \
            {date:last_activity_date} \
            {owner.display_name} {owner.reputation} \

 \

 \

 '
    },
    filters: {
        date: function(value) {
            var date = new Date(parseInt(value)*1000);
            return date.getFullYear() + "-" + pad(date.getMonth()+1) + "-" + pad(date.getDate());
        },
        template: function(value, templateName) {
            var template = su.templates[templateName],
                html = "";
            if (template) {
                for (i = 0, l = value.length; i < l; i++) {
                    html += applyTemplate(template, value[i]);
                }
            }
            return html;
        },
        acceptedclass: function(value) {
            return value.accepted_answer_id ? "se-answered-accepted" :
                (value.answer_count ? "se-answered" : "se-unanswered");
        },
        viewcountnumber: function(value) {
            return value > 999 ? Math.round(value/1000) : value;
        },
        viewcountk: function(value) {
            return value > 999 ? "k" : "";
        },
        viewcountcolor: function(value) {
            if(value >= 100000)
                return "se-views-100k";
            else if (value >= 10000)
                return "se-views-10k";
            else if (value >= 1000)
                return "se-views-1k"
            else
                return "";
        },
        ifdef: function(value) {
            return typeof value === "undefined" ? "none" : "";
        }
    }
};

domLoaded();

})();
<commit_msg>checking in template updates<commit_after>
(function() {

// some API urls
var google = "http://ajax.googleapis.com/ajax/services/search/web?v=1.0&callback={callback}&rsz=large&q={query}",
    questions = "http://api.stackoverflow.com/1.0/questions/{id}?key={key}&jsonp={callback}",
    questionsTagged = "http://api.stackoverflow.com/1.0/questions/?key={key}&tagged={tagged}&jsonp={callback}",
    search = "http://api.stackoverflow.com/1.0/search/?key={key}&intitle={intitle}&nottagged={nottagged}&tagged={tagged}&jsonp={callback}",
    unansweredQuestionsTagged = "http://api.stackoverflow.com/1.0/questions/unanswered/?key={key}&tagged={tagged}&jsonp={callback}",
    questionsByUser = "http://api.stackoverflow.com/1.0/users/{id}/questions?key={key}&jsonp={callback}",
    // prevent loading more than once
    isLoaded,
    // each call creates a unique jsonp callback
    jsonpCount = 0,
    // for matching the question id inside a stackexchange question url
    regexQuestion = /\/questions\/([0-9]*)\//ig,
    // those listening for the loaded event
    callbacks = [];

function domLoaded() {
    // based on the proven method of simulating DOMContentLoaded in IE
    if (window.addEventListener) {
        window.addEventListener("load", loaded, false);
    }
    else {
        window.attachEvent("onload", loaded);
    }
    var check;
    if (window.attachEvent) {
        if ((window == window.top) && document.documentElement.doScroll) {
            var timeout, er, el = document.createElement("div");
            check = function() {
                try {
                    el.doScroll("left");
                }
                catch (er) {
                    timeout = window.setTimeout(check, 0);
                    return;
                }
                el = null;
                loaded();
            }
            check();
        }
        else {
            document.onreadystatechange = function() {
                if (/loaded|complete/.test(document.readyState)) {
                    loaded();
                }
            };
        }
    }
    else if (document.addEventListener) {
        document.addEventListener("DOMContentLoaded", loaded, false);
    }
}

function loaded() {
    // called when the DOM is ready
    if (!isLoaded) {
        isLoaded = true;
        for (var i = 0, l = callbacks.length; i < l; i++) {
            callbacks[i]();
        }
    }
}

function jsonp(url, params, callback) {
    // begin a jsonp request and foward the response
    var jsonp = "_jsonp" + jsonpCount++;
    url = url
        .replace("{callback}", "stackunderflow." + jsonp)
        .replace("{key}", su.appId);
    if (params) {
        for (var name in params) {
            url = url.replace("{" + name + "}", params[name]);
        }
    }
    su[jsonp] = function(data) {
        delete su[jsonp];
        su.loaded(function() {
            callback(data);
        });
    };
    var scr = document.createElement("script");
    scr.type = "text/javascript";
    scr.src = url;
    // insertBefore in case this is performed inline in the head
    var head = document.getElementsByTagName("head")[0];
    head.insertBefore(scr, head.firstChild);
}

function getUniqueQuestions(searchResults) {
    // created an array of unique question ids
    // from a set of google search results
    var questions = [], index = {};
    for (var i = 0, l = searchResults.length; i < l; i++) {
        regexQuestion.lastIndex = 0;
        var url = searchResults[i].unescapedUrl,
            match = regexQuestion.exec(url);
        if ( match && match[1] ) {
            var questionId = parseInt(match[1]);
            if (!index[questionId]) {
                questions.push(questionId);
                index[questionId] = 1;
            }
        }
    }
    return questions;
}

function pad(num) {
    var s = num+"";
    return s.length === 2 ? s : ("0" + s);
}
function applyTemplate(html, obj) {
    var i, l, regexTokens = new RegExp("\\{([^}]*)\\}", "g"), match, token,
        tokenValues = {
            site: su.site
        };
    do {
        match = regexTokens.exec(html);
        token = match ? match[1] : null;
        if (token) {
            var index = token.indexOf(":"),
                filter = null;
            if (index > -1) {
                filter = token.substr(0, index);
                token = token.substr(index + 1);
            }
            var parts = token === "=" ? [] : token.split('.'),
                step = obj;
            for (var i = 0, l = parts.length; i < l; i++) {
                step = step[parts[i]];
                if (typeof step === "undefined") break;
            }
            if (filter) {
                var template;
                if (filter.indexOf("template-") > -1) {
                    template = filter.substr("template-".length);
                    filter = "template";
                }
                filter = su.filters[filter];
                if (filter) {
                    step = filter(step, template);
                }
            }
            if (typeof step !== "undefined") {
                tokenValues[match[1]] = step;
            }
        }
    }
    while (token);
    for (token in tokenValues) {
        html = html.replace(new RegExp("\\{" + token.replace(/([\.\:\-])/g, '\\$1') + "\\}","g"), tokenValues[token]);
    }
    return html;
}

function append(target, html) {
    var div = document.createElement("div");
    div.innerHTML = html;
    while(div.firstChild) {
        target.appendChild(div.firstChild);
    }
}

function execQuestions(url, params, complete) {
    var renderArgs,
        ctx = { render: function() { renderArgs = arguments; } };
    jsonp(url, params, function(questions) {
        if (complete) complete(questions);
        if (renderArgs) {
            renderArgs = Array.prototype.slice.apply(renderArgs);
            renderArgs.splice(0, 0, questions);
            su.render.questions.apply(null, renderArgs);
        }
    });
    return ctx;
}

var su = window.stackunderflow = {
    appId: "oxXcnoD51kKE-crj7TadaA",
    site: "http://stackoverflow.com",
    loaded: function(callback) {
        if (isLoaded) {
            callback();
        }
        else {
            callbacks.push(callback);
        }
    },
    googleQuestions: function(term, complete) {
        var renderArgs,
            ctx = { render: function() { renderArgs = arguments; } };
        var site = su.site + "/questions";
        term = term || ('"' + window.location + '"');
        jsonp(google, {
            query: "site:" + site + " " + term
        },
        function(data) {
            var questions = getUniqueQuestions(data.responseData.results);
            if (questions.length) {
                su.getQuestions(questions, function(questions) {
                    if (complete) complete(questions);
                    if (renderArgs) {
                        renderArgs = Array.prototype.slice.apply(renderArgs);
                        renderArgs.splice(0, 0, questions);
                        su.render.questions.apply(null, renderArgs);
                    }
                });
            }
        });
        return ctx;
    },
    getQuestions: function(questionIds, complete) {
        return execQuestions(questions, { id: questionIds.join(';') }, complete);
    },
    searchQuestions: function(intitle, tagged, notTagged, complete) {
        return execQuestions(search, { tagged: tagged, nottagged: notTagged, intitle: intitle }, complete);
    },
    getQuestionsWithTags: function(tags, onlyUnanswered, complete) {
        return execQuestions(onlyUnanswered ? unansweredQuestionsTagged : questionsTagged,
            { tagged: tags }, complete);
    },
    getQuestionsByUser: function(userIds, complete) {
        return execQuestions(questionsByUser, { id: userIds instanceof Array ? userIds.join(';') : userIds }, complete);
    },
    render: {
        questions: function(questions, target, template) {
            template = template || "question";
            if (typeof target === "string") {
                if (target.charAt(0) === "#") target = target.substr(1);
                target = document.getElementById(target);
            }
            var html = "";
            questions = questions.questions;
            if (questions) {
                for (var i = 0, l = questions.length; i < l; i++) {
                    html += applyTemplate(su.templates[template], questions[i]);
                }
            }
            append(target, html);
        }
    },
    templates: {
        tag: '{=} ',
        question: '
 \

 \

 \

 \

 \

 \
                    {up_vote_count} \

votes
 \

 \

 \

 \
                {answer_count}answers \

 \

 \

{viewcountk:view_count} views
 \

 \

 \

{title}
 \

 \
            {template-tag:tags}\

 \

 \

asked {date:last_activity_date}
 \

{owner.display_name} {owner.reputation}
 \

 \

 \

 \

 '
    },
    filters: {
        date: function(value) {
            var date = new Date(parseInt(value)*1000);
            return date.getFullYear() + "-" + pad(date.getMonth()+1) + "-" + pad(date.getDate());
        },
        template: function(value, templateName) {
            var template = su.templates[templateName],
                html = "";
            if (template) {
                for (i = 0, l = value.length; i < l; i++) {
                    html += applyTemplate(template, value[i]);
                }
            }
            return html;
        },
        acceptedclass: function(value) {
            return value.accepted_answer_id ? "se-answered-accepted" :
                (value.answer_count ? "se-answered" : "se-unanswered");
        },
        viewcountnumber: function(value) {
            return value > 999 ? Math.round(value/1000) : value;
        },
        viewcountk: function(value) {
            return value > 999 ? "k" : "";
        },
        viewcountcolor: function(value) {
            if(value >= 100000)
                return "se-views-100k";
            else if (value >= 10000)
                return "se-views-10k";
            else if (value >= 1000)
                return "se-views-1k"
            else
                return "";
        },
        ifdef: function(value) {
            return typeof value === "undefined" ? "none" : "";
        }
    }
};

domLoaded();

})();
<|endoftext|>


<commit_before>import { hashToCollection } from '../../libs/hashToCollection';
import _ from 'underscore';
import generateFeedbackString from './generateFeedbackString.js';
import {
  incrementResponseCount,
  submitNewResponse,
  findResponseByText
} from '../../actions/responses.js';

const getLatestAttempt = function (attempts = []) {
  const lastIndex = attempts.length - 1;
  return attempts[lastIndex];
};

export default function updateResponseResource(returnValue, questionID, attempts, dispatch, playQuestion) {
  let previousAttempt;

  const preAtt = getLatestAttempt(attempts);
  if (preAtt) { previousAttempt = getLatestAttempt(attempts).response; }
  const prid = previousAttempt ? previousAttempt.key : undefined;
  const isFirstAttempt = !preAtt

  if (returnValue.found && returnValue.response.key) {
    dispatch(
      incrementResponseCount(questionID, returnValue.response.key, prid, isFirstAttempt)
    );
  } else {
    incrementOrCreateResponse(questionID, returnValue, prid, dispatch, isFirstAttempt);
  }
}

export function incrementOrCreateResponse(questionID, returnValue, prid, dispatch, isFirstAttempt) {
  const callback = (response) => {
    if (response) {
      dispatch(
        incrementResponseCount(questionID, response.key, prid, isFirstAttempt)
      );
    } else {
      dispatch(
        submitNewResponse(returnValue.response, prid, isFirstAttempt)
      );
    }
  };
  findResponseByText(returnValue.response.text, questionID, callback);
}
<commit_msg>removes deprecated code from update responses<commit_after>import { hashToCollection } from '../../libs/hashToCollection';
import _ from 'underscore';
import generateFeedbackString from './generateFeedbackString.js';
import {
  incrementResponseCount,
  submitResponse,
  findResponseByText
} from '../../actions/responses.js';

const getLatestAttempt = function (attempts = []) {
  const lastIndex = attempts.length - 1;
  return attempts[lastIndex];
};

export default function updateResponseResource(returnValue, questionID, attempts, dispatch, playQuestion) {
  let previousAttempt;
  const preAtt = getLatestAttempt(attempts);
  if (preAtt) { previousAttempt = getLatestAttempt(attempts).response; }
  const prid = previousAttempt ? previousAttempt.key : undefined;
  const isFirstAttempt = !preAtt;
  dispatch(
    submitResponse(returnValue.response, prid, isFirstAttempt)
  );
}
<|endoftext|>


<commit_before>
require('../components/validated-payment-form-component');

App.ValidatedPaymentFormCheckoutComponent = App.ValidatedPaymentFormComponent.extend({

  credits: Ember.computed.alias('currentUser.walletBalance'),
  total: Ember.computed.alias('currentUser.nextBox.total'),
  showPaymentForm: true,

  creditsCoverTotal: function () {
    var covered = this.get('credits') > this.get('total');
    return covered;
  }.property('credits', 'total'),

  // For some reason, property does not work here,
  // so we add an observer below.
  watchSkipPaymentMethod: function () {
    if (!this.get('creditsCoverTotal')) {
      this.set('showPaymentForm', true);
    } else {
      this.set('showPaymentForm', !parseInt(this.get('user.createSubscriptionAction.skipPaymentMethod')));
    }
  }.observes('user.createSubscriptionAction.skipPaymentMethod'),

  walletBalance: function () {
    var walletBalance = this.get('credits') > 0;
    return walletBalance;
  }.property('credits')

});
<commit_msg>More showPaymentForm refinements<commit_after>
require('../components/validated-payment-form-component');

App.ValidatedPaymentFormCheckoutComponent = App.ValidatedPaymentFormComponent.extend({

  credits: Ember.computed.alias('currentUser.walletBalance'),
  total: Ember.computed.alias('currentUser.nextBox.total'),

  creditsCoverTotal: function () {
    return this.get('credits') > this.get('total');
  }.property('credits', 'total'),

  // For some reason, property does not work here,
  // so we add an observer below.
  watchSkipPaymentMethod: function () {
    if (this.get('creditsCoverTotal')) {
      this.set('showPaymentForm', !this.get('skipPaymentMethod'));
    } else {
      this.set('showPaymentForm', true);
    }
  }.observes('skipPaymentMethod', 'creditsCoverTotal'),

  showPaymentForm: function () {
    return !this.get('skipPaymentMethod') || this.get('creditsCoverTotal');
  }.property('creditsCoverTotal', 'skipPaymentMethod'),

  skipPaymentMethod: function () {
    return parseInt(this.get('user.createSubscriptionAction.skipPaymentMethod'));
  }.property('user.createSubscriptionAction.skipPaymentMethod'),

  walletBalance: function () {
    var walletBalance = this.get('credits') > 0;
    return walletBalance;
  }.property('credits')

});
<|endoftext|>


<commit_before>/* eslint-disable no-unused-vars*/
/* eslint-disable no-undef*/
/* eslint-disable no-dupe-keys*/
/* eslint-disable no-multi-assign*/
const invertedIndex = new InvertedIndex();
const books = require('../books.json');
const empty = require('../emptyBook.json');
const invalid = require('../invalidBook.json');
const little = require('../littleBook.json');
describe("Inverted Index", () => {

  /**
   * Test suite to ensure the validateFile method returns an object of
   * the correct index mapping
   */

  describe('Read book data', () => {
    it('Should ensure file content is actually a valid JSON array', () => {
      expect(InvertedIndex.validateFile(books)).toEqual(true);
    });

    it('Should return false for invalid JSON file', () => {
      expect(InvertedIndex.validateFile(invalid)).toEqual(false);
    });

    it('Should ensure that JSON array is not empty', () => {
      expect(InvertedIndex.validateFile(empty)).toEqual(false);
    });
  });

  /*
   * Populate Index Test Suite
   */

  describe('Populate Index', () => {
    invertedIndex.createIndex('books.json', books);

    it('Should ensure that index is created once the file has been read', () => {
      expect(invertedIndex.indices['books.json']).toBeDefined();
    });

    it('Should map the string keys to the correct objects', () => {
      expect(invertedIndex.indices['books.json'].terms.alice).toEqual([1]);
      expect(invertedIndex.indices['books.json'].terms.and).toEqual([1, 2]);
      expect(invertedIndex.indices['books.json'].terms.lord).toEqual([2]);
    });

    invertedIndex.createIndex('littleBook.json', little);
    it('Should return an accurate index of the content of the json file', () => {
      expect(invertedIndex.getIndex('littleBook.json').terms.is).toEqual([1, 2]);
    });

  });

  /**
   * Test suite to ensure the getIndex method returns an object of
   * the correct index mapping
   */

  describe('Get index', () => {
    it('Should return an object when value is found', () => {
      const indexedFile = invertedIndex.getIndex('books.json');
      expect(typeof(indexedFile) === 'object').toBeTruthy();
    });

    it('Should contain valid indexed words and position', () => {
      expect(invertedIndex.getIndex('books.json').terms.alice).toEqual([1]);
      expect(invertedIndex.getIndex('books.json').terms.and).toEqual([1, 2]);
      expect(invertedIndex.getIndex('books.json').terms.lord).toEqual([2]);
    });
  });

  /**
   * Test suite to ensure the tokenizer method returns an object of
   * the correct index mapping
   */
  describe('Tokenizer', () => {
    it('Should return an array when a string is passed', () => {
      expect(InvertedIndex.tokenizer('This is yemi')).toEqual(['this', 'is', 'yemi']);
    });
    it('Should return an array when a recursive array is passed', () => {
      expect(InvertedIndex.tokenizer('books.json', ['a', 'alice'], 'book', 'me'))
        .toEqual(['books', 'json']);
    });
  });

  /**
   * Test suite to ensure the searchIndex method returns an object of
   * the correct index mapping
   */

  describe('Search Index', () => {
    invertedIndex.searchIndex('../books.json', 'alice');
    it('Should return correct index of the search term', () => {
      expect(invertedIndex.searchIndex('books.json', 'alice, a')[0]).toEqual({
        terms: {
          alice: [1],
          a: [1, 2]
        },
        count: 2,
        fileName: 'books.json'
      });
    });

    it('Should return books.json:[] when no result is found',
      () => {
        expect(invertedIndex.searchIndex('books.json', 'along')).toEqual(false);
      });

    it('Should return correct index in an array search terms', () => {
      expect(invertedIndex.searchIndex('books.json', 'alice, [hole,[a]]')[0]).toEqual({
        terms: {
          alice: [1],
          hole: [1],
          a: [1, 2]
        },
        count: 2,
        fileName: 'books.json'
      });
    });
    it('Should ensure searchIndex can handle a number of varied search terms', () => {
      expect(invertedIndex.searchIndex('littleBook.json', '[is, [ali, book, me, [help, me, out]]]')).toEqual([{
        terms: {
          is: [1, 2],
          ali: [1],
          me: [2]
        },
        count: 2,
        fileName: 'littleBook.json'
      }]);
    });
    it('Should go through all indexed terms if fileName is not passed', () => {
      expect(invertedIndex.searchIndex(null, 'alice, ali')).toEqual([{
        terms: {
          alice: [1]
        },
        count: 2,
        fileName: 'books.json'
      }, {
        terms: {
          ali: [1]
        },
        count: 2,
        fileName: 'littleBook.json'
      }])
    })
  });
});
<commit_msg>adjust(wrap): manually limit number of characters on one line<commit_after>/* eslint-disable no-unused-vars*/
/* eslint-disable no-undef*/
/* eslint-disable no-dupe-keys*/
/* eslint-disable no-multi-assign*/
const invertedIndex = new InvertedIndex();
const books = require('../books.json');
const empty = require('../emptyBook.json');
const invalid = require('../invalidBook.json');
const little = require('../littleBook.json');
describe("Inverted Index", () => {

  /**
   * Test suite to ensure the validateFile method returns an object of
   * the correct index mapping
   */

  describe('Read book data', () => {
    it('Should ensure file content is actually a valid JSON array', () => {
      expect(InvertedIndex.validateFile(books)).toEqual(true);
    });

    it('Should return false for invalid JSON file', () => {
      expect(InvertedIndex.validateFile(invalid)).toEqual(false);
    });

    it('Should ensure that JSON array is not empty', () => {
      expect(InvertedIndex.validateFile(empty)).toEqual(false);
    });
  });

  /*
   * Populate Index Test Suite
   */

  describe('Populate Index', () => {
    invertedIndex.createIndex('books.json', books);

    it('Should ensure that index is created once the file has been read', () => {
      expect(invertedIndex.indices['books.json']).toBeDefined();
    });

    it('Should map the string keys to the correct objects', () => {
      expect(invertedIndex.indices['books.json'].terms.alice).toEqual([1]);
      expect(invertedIndex.indices['books.json'].terms.and).toEqual([1, 2]);
      expect(invertedIndex.indices['books.json'].terms.lord).toEqual([2]);
    });

    invertedIndex.createIndex('littleBook.json', little);
    it('Should return an accurate index of the content of the json file', () => {
      expect(invertedIndex.getIndex('littleBook.json').terms.is).toEqual([1, 2]);
    });

  });

  /**
   * Test suite to ensure the getIndex method returns an object of
   * the correct index mapping
   */

  describe('Get index', () => {
    it('Should return an object when value is found', () => {
      const indexedFile = invertedIndex.getIndex('books.json');
      expect(typeof(indexedFile) === 'object').toBeTruthy();
    });

    it('Should contain valid indexed words and position', () => {
      expect(invertedIndex.getIndex('books.json').terms.alice).toEqual([1]);
      expect(invertedIndex.getIndex('books.json').terms.and).toEqual([1, 2]);
      expect(invertedIndex.getIndex('books.json').terms.lord).toEqual([2]);
    });
  });

  /**
   * Test suite to ensure the tokenizer method returns an object of
   * the correct index mapping
   */
  describe('Tokenizer', () => {
    it('Should return an array when a string is passed', () => {
      expect(InvertedIndex.tokenizer('This is yemi')).toEqual(['this', 'is', 'yemi']);
    });
    it('Should return an array when a recursive array is passed', () => {
      expect(InvertedIndex.tokenizer('books.json', ['a', 'alice'], 'book', 'me'))
        .toEqual(['books', 'json']);
    });
  });

  /**
   * Test suite to ensure the searchIndex method returns an object of
   * the correct index mapping
   */

  describe('Search Index', () => {
    invertedIndex.searchIndex('../books.json', 'alice');
    it('Should return correct index of the search term', () => {
      expect(invertedIndex.searchIndex('books.json', 'alice, a')[0]).toEqual({
        terms: {
          alice: [1],
          a: [1, 2]
        },
        count: 2,
        fileName: 'books.json'
      });
    });

    it('Should return books.json:[] when no result is found',
      () => {
        expect(invertedIndex.searchIndex('books.json', 'along')).toEqual(false);
      });

    it('Should return correct index in an array search terms', () => {
      expect(invertedIndex.searchIndex('books.json', 'alice, [hole,[a]]')[0])
        .toEqual({
          terms: {
            alice: [1],
            hole: [1],
            a: [1, 2]
          },
          count: 2,
          fileName: 'books.json'
        });
    });
    it('Should ensure searchIndex handles a number of varied search terms', () => {
      expect(invertedIndex
          .searchIndex('littleBook.json', '[is, [ali, book, me, [help, me, out]]]'))
        .toEqual([{
          terms: {
            is: [1, 2],
            ali: [1],
            me: [2]
          },
          count: 2,
          fileName: 'littleBook.json'
        }]);
    });
    it('Should go through all indexed terms if fileName is not passed', () => {
      expect(invertedIndex.searchIndex(null, 'alice, ali')).toEqual([{
        terms: {
          alice: [1]
        },
        count: 2,
        fileName: 'books.json'
      }, {
        terms: {
          ali: [1]
        },
        count: 2,
        fileName: 'littleBook.json'
      }])
    })
  });
});
<|endoftext|>