<commit_before>// @flow

// danger (removed by danger)
import {danger, schedule, markdown, warn, fail} from 'danger'

// danger plugins
import yarn from 'danger-plugin-yarn'

async function main() {
  const taskName = String(process.env.task)

  switch (taskName) {
    case 'ANDROID':
      await runAndroid()
      break
    case 'IOS':
      await runiOS()
      break
    case 'GREENKEEPER':
      await runGreenkeeper()
      break
    case 'JS-general':
      await runJSのGeneral()
      await yarn()
      break
    case 'JS-data':
      await runJSのData()
      break
    case 'JS-flow':
      await runJSのFlow()
      break
    case 'JS-jest':
      await runJSのJest()
      break
    case 'JS-lint':
      await runJSのLint()
      break
    case 'JS-prettier':
      await runJSのPrettier()
      break
    default:
      warn(`Unknown task name "${taskName}"; Danger cannot report anything.`)
  }
}

//
// task=GREENKEEPER
//

function runGreenkeeper() {
  // message('greenkeeper ran')
}

//
// task=ANDROID
//

function runAndroid() {
}

//
// task=IOS
//

function runiOS() {
}

//
// task=JS-data
//

async function runJSのData() {
  await runJSのDataのData()
  await runJSのDataのBusData()
}

function runJSのDataのData() {
}

function runJSのDataのBusData() {
}

//
// task=JS-general
//

async function runJSのGeneral() {
  await flowAnnotated()
  await bigPr()
  await exclusionaryTests()
}

// New js files should have `@flow` at the top
function flowAnnotated() {
  danger.git.created_files
    .filter(path => path.endsWith('.js'))
    // except for those in /flow-typed
    .filter(filepath => !filepath.includes('flow-typed'))
    .filter(filepath => !readFile(filepath).includes('@flow'))
    .forEach(file =>
      warn(`<code>${file}</code> has no <code>@flow</code> annotation!`),
    )
}

// Warn if tests have been enabled to the exclusion of all others
function exclusionaryTests() {
  danger.git.created_files
    .filter(filepath => filepath.endsWith('.test.js'))
    .map(filepath => ({filepath, content: readFile(filepath)}))
    .filter(
      ({content}) =>
        content.includes('it.only') || content.includes('describe.only'),
    )
    .forEach(({filepath}) =>
      warn(
        `An <code>only</code> was left in ${filepath} – no other tests can run.`,
      ),
    )
}

// Warn when PR size is large (mainly for hawken)
function bigPr() {
  const bigPRThreshold = 400 // lines
  const thisPRSize = danger.github.pr.additions + danger.github.pr.deletions
  if (thisPRSize > bigPRThreshold) {
    warn(
      h.details(
        h.summary(
          `Big PR! We like to try and keep PRs under ${bigPRThreshold} lines, and this one was ${thisPRSize} lines.`,
        ),
        h.p(
          'If the PR contains multiple logical changes, splitting each change into a separate PR will allow a faster, easier, and more thorough review.',
        ),
      ),
    )
  }
}

async function gradle() {
  await buildDotGradle()
  await mainDotJava()
  await settingsDotGradleSpacing()
}

// Ensure that the build.gradle dependencies list is sorted
function buildDotGradle() {
}

// Ensure that the MainApplication.java imports list is sorted
function mainDotJava() {
}

// Enforce spacing in the settings.gradle file
function settingsDotGradleSpacing() {
}

//
// task=JS-flow
//

function runJSのFlow() {
}

//
// JS-jest
//

function runJSのJest() {
}

//
// JS-lint
//

function runJSのLint() {
}

//
// JS-prettier
//

function runJSのPrettier() {
}

//
// Utilities
//

//
// Run the file
//
schedule(main)
<commit_msg>add h.() and m.() utilities for generating html/markdown strings for danger<commit_after>// @flow

// danger (removed by danger)
import {danger, schedule, markdown, warn, fail} from 'danger'

// danger plugins
import yarn from 'danger-plugin-yarn'

async function main() {
  const taskName = String(process.env.task)

  switch (taskName) {
    case 'ANDROID':
      await runAndroid()
      break
    case 'IOS':
      await runiOS()
      break
    case 'GREENKEEPER':
      await runGreenkeeper()
      break
    case 'JS-general':
      await runJSのGeneral()
      await yarn()
      break
    case 'JS-data':
      await runJSのData()
      break
    case 'JS-flow':
      await runJSのFlow()
      break
    case 'JS-jest':
      await runJSのJest()
      break
    case 'JS-lint':
      await runJSのLint()
      break
    case 'JS-prettier':
      await runJSのPrettier()
      break
    default:
      warn(`Unknown task name "${taskName}"; Danger cannot report anything.`)
  }
}

//
// task=GREENKEEPER
//

function runGreenkeeper() {
  // message('greenkeeper ran')
}

//
// task=ANDROID
//

function runAndroid() {
}

//
// task=IOS
//

function runiOS() {
}

//
// task=JS-data
//

async function runJSのData() {
  await runJSのDataのData()
  await runJSのDataのBusData()
}

function runJSのDataのData() {
}

function runJSのDataのBusData() {
}

//
// task=JS-general
//

async function runJSのGeneral() {
  await flowAnnotated()
  await bigPr()
  await exclusionaryTests()
}

// New js files should have `@flow` at the top
function flowAnnotated() {
  danger.git.created_files
    .filter(path => path.endsWith('.js'))
    // except for those in /flow-typed
    .filter(filepath => !filepath.includes('flow-typed'))
    .filter(filepath => !readFile(filepath).includes('@flow'))
    .forEach(file =>
      warn(`<code>${file}</code> has no <code>@flow</code> annotation!`),
    )
}

// Warn if tests have been enabled to the exclusion of all others
function exclusionaryTests() {
  danger.git.created_files
    .filter(filepath => filepath.endsWith('.test.js'))
    .map(filepath => ({filepath, content: readFile(filepath)}))
    .filter(
      ({content}) =>
        content.includes('it.only') || content.includes('describe.only'),
    )
    .forEach(({filepath}) =>
      warn(
        `An <code>only</code> was left in ${filepath} – no other tests can run.`,
      ),
    )
}

// Warn when PR size is large (mainly for hawken)
function bigPr() {
  const bigPRThreshold = 400 // lines
  const thisPRSize = danger.github.pr.additions + danger.github.pr.deletions
  if (thisPRSize > bigPRThreshold) {
    warn(
      h.details(
        h.summary(
          `Big PR! We like to try and keep PRs under ${bigPRThreshold} lines, and this one was ${thisPRSize} lines.`,
        ),
        h.p(
          'If the PR contains multiple logical changes, splitting each change into a separate PR will allow a faster, easier, and more thorough review.',
        ),
      ),
    )
  }
}

async function gradle() {
  await buildDotGradle()
  await mainDotJava()
  await settingsDotGradleSpacing()
}

// Ensure that the build.gradle dependencies list is sorted
function buildDotGradle() {
}

// Ensure that the MainApplication.java imports list is sorted
function mainDotJava() {
}

// Enforce spacing in the settings.gradle file
function settingsDotGradleSpacing() {
}

//
// task=JS-flow
//

function runJSのFlow() {
}

//
// JS-jest
//

function runJSのJest() {
}

//
// JS-lint
//

function runJSのLint() {
}

//
// JS-prettier
//

function runJSのPrettier() {
}

//
// Utilities
//

export const h /*: any*/ = new Proxy(
  {},
  {
    get(_, property) {
      return function(...children /*: Array<string>*/) {
        if (!children.length) {
          return `<${property}>`
        }
        return `<${property}>${children.join('\n')}</${property}>`
      }
    },
  },
)

export const m = {
  code(attrs /*: Object*/, ...children /*: Array<string>*/) {
    return (
      '\n' +
      '```' +
      (attrs.language || '') +
      '\n' +
      children.join('\n') +
      '\n' +
      '```' +
      '\n'
    )
  },
  json(blob /*: any*/) {
    return m.code({language: 'json'}, JSON.stringify(blob, null, 2))
  },
}

//
// Run the file
//
schedule(main)
<|endoftext|>


<commit_before>import { range } from 'lodash';
import moment from 'moment-timezone';

context('Assignment Edit', () => {
  const fillDetails = () => {
    cy.get('.heading').should('contain.text', 'STEP 1')
    cy.get('.controls .btn-primary').should('be.disabled')
    cy.get('input[name="title"]').type('test assignment #1')
    cy.get('.controls .btn-primary').should('not.be.disabled')
  }
  const addTemplate = ({ name, dueDateOffsetDays = '1', dueTimeHour = '5',
    dueTimeMinutes = '30', closesDateOffsetDays = '30', isAM = false, doSelect = false }) => {
    cy.get('[data-test-id="grading-templates"]').click()
    cy.get('[data-test-id="add-template"]').click()
    cy.get('.modal').should('be.visible')
    cy.get('.modal input[name="name"]').type(name)
    cy.get('.modal select[name="default_due_date_offset_days"]').select(dueDateOffsetDays);
    cy.get('.modal select[name="default_due_time_hour"]').select(dueTimeHour);
    cy.get('.modal select[name="default_due_time_minute"]').select(dueTimeMinutes);
    cy.get('.modal select[name="default_close_date_offset_days"]').select(closesDateOffsetDays);
    cy.get('.modal input[name="default_due_time_ampm"]').check(isAM ? 'am' : 'pm', { force: true });
    cy.get('.modal [type="submit"]').click()

    if(doSelect) {
      cy.get('[data-test-id="grading-templates"]').click()
      cy.get(`[data-test-id="${name}"]`).click()
    }
  }

  it('loads and advances homework', () => {
    cy.visit('/course/2/assignment/edit/homework/new')
    cy.disableTours();
    fillDetails()
    cy.get('.controls .btn-primary').click()
    cy.get('.heading').should('contain.text', 'STEP 2')
    cy.get('.controls .btn-primary').should('be.disabled')
    cy.get('.chapter[data-is-expanded="false"]').first().click()
    cy.get('[data-chapter-section="2.1"]').click();
    cy.get('[data-chapter-section="2.2"]').click();
    cy.get('.controls .btn-primary').should('not.be.disabled')

    cy.get('.controls .btn-primary').click()
    cy.get('.heading').should('contain.text', 'STEP 3')
    cy.get('.controls .btn-primary').should('be.disabled')
    range(3).map(i => cy.get('.exercise-card').eq(i).find('.action.include').trigger('click', { force: true }))
    cy.get('.controls .btn-primary').should('not.be.disabled')

    cy.get('.controls .btn-primary').click()
    cy.get('.heading').should('contain.text', 'STEP 4')
    cy.get('.openstax-exercise-preview').should('have.length', 3)

    cy.get('button.ox-icon-arrow-up').eq(1).click({ force: true })
  });

  it('loads and advances reading', () => {
    cy.visit('/course/1/assignment/edit/reading/new')
    cy.disableTours();
    fillDetails()
    cy.get('.controls .btn-primary').click()
    cy.get('.heading').should('contain.text', 'STEP 2')
    cy.get('.controls .btn-primary').should('be.disabled')
    cy.get('.chapter[data-is-expanded="false"]').first().click()
    cy.get('[data-chapter-section="2.1"]').click();
    cy.get('[data-chapter-section="2.2"]').click();
    cy.get('[data-chapter-section="2.3"]').click();
    cy.get('.controls .btn-primary').should('not.be.disabled')

    cy.get('.controls .btn-primary').click()
    cy.get('.heading').should('contain.text', 'STEP 3')

    cy.get('.selected-section .chapter-section').invoke('text').then(labels => {
      expect(labels).to.eq(['2.1', '2.2', '2.3'].join(''))
    })
    cy.get('button.ox-icon-arrow-up').eq(1).click()
    cy.get('.selected-section .chapter-section').invoke('text').then(labels => {
      expect(labels).to.eq(['2.1', '2.3', '2.2'].join(''))
    })
  });

  it('increases selections', () => {
    cy.visit('/course/2/assignment/edit/homework/new')
    cy.disableTours()
    fillDetails()
    cy.get('.controls .btn-primary').click()
    cy.get('.chapter-checkbox .btn').first().click()
    cy.get('.controls .btn-primary').click()
    cy.get('[data-test-id="selection-count"]').should('contain.text', '0')
    cy.get('[data-test-id="selection-count-footer"]').should('contain.text', '0')

    cy.get('.action.include:first').click({ force: true })

    cy.get('[data-test-id="selection-count"]').should('contain.text', '1')
    cy.get('[data-test-id="selection-count-footer"]').should('contain.text', '1')
    cy.get('[data-test-id="tutor-count"]').should('contain.text', '3')
    cy.get('[data-test-id="tutor-count-footer"]').should('contain.text', '3')
    cy.get('[data-test-id="total-count"]').should('contain.text', '4')
    cy.get('[data-test-id="total-count-footer"]').should('contain.text', '4')
  });

  it('filters question types', () => {
    cy.visit('/course/2/assignment/edit/homework/new')
    cy.disableTours()
    fillDetails()
    cy.get('.controls .btn-primary').click()
    cy.get('.chapter-checkbox .btn').first().click()
    cy.get('.controls .btn-primary').click()
    cy.get('[name="filter"][value="oe"]').click({ force: true })
    cy.get('[data-section]').should('not.contain.text', 'Multiple Choice Questions')
    cy.get('[name="filter"][value="mc"]').click({ force: true })
    cy.get('[data-section]').should('not.contain.text', 'Written Response Questions')
  });

  it('can change course timezone', () => {
    cy.visit('/course/2/assignment/edit/homework/new')
    cy.disableTours()
    fillDetails()
    cy.get('[data-test-id="change-timezone"]').click()
    cy.get('.settings-edit-course-modal').should('be.visible')
    cy.get('.settings-edit-course-modal [value="Hawaii"]').click({ force: true })
    cy.get('.settings-edit-course-modal .async-button').click()
    cy.get('.settings-edit-course-modal').should('not.exist')
    cy.get('[data-test-id="change-timezone"').should( 'contain.text', 'Hawaii')
  });

  it('renders external assignment', () => {
    cy.visit('/course/2/assignment/edit/external/new')
    cy.disableTours()
    cy.get('.heading').should('not.contain.text', 'STEP 1')
    cy.get('[name="settings.external_url"').type('url')
  });

  it('can add a new template', () => {
    const templateName = 'This is a new template'
    cy.visit('/course/2/assignment/edit/homework/new')
    cy.disableTours()
    fillDetails()
    addTemplate({ name: templateName })
    cy.get('[data-test-id="grading-templates"]').should('contain.text', templateName)
  });

  it('can select another template and update dates', () => {
    const templateName = 'Template to update dates'
    const dueDateOffsetDays = '3', dueTimeHour = '7', dueTimeMinutes = '15', closesDateOffsetDays = '10', isAM = false
    cy.visit('/course/2/assignment/edit/homework/new')
    cy.disableTours()
    fillDetails()
    addTemplate({ name: templateName, dueDateOffsetDays, dueTimeHour, dueTimeMinutes, closesDateOffsetDays, isAM, doSelect: true })
    cy.get('input[name="tasking_plans[0].opens_at"]').then(o => {
      const openDate = o[0].defaultValue
      let updatedDueDate;
      // Due date should change
      cy.get('input[name="tasking_plans[0].due_at"]').then(d => {
        const dueDate = moment(d[0].defaultValue).toISOString();
        updatedDueDate = dueDate;
        // Compute the due date from the open date
        const hour = isAM ? parseInt(dueTimeHour, 10) : parseInt(dueTimeHour, 10) + 12
        const expectedDueDate = moment(openDate)
          .add(parseInt(dueDateOffsetDays, 10), 'days')
          .set({ hour, minutes: parseInt(dueTimeMinutes, 10) })
          .toISOString();
        expect(dueDate).eq(expectedDueDate)
      })
      // Closes date should change
      cy.get('input[name="tasking_plans[0].closes_at"]').then(c => {
        const closesDate = moment(c[0].defaultValue).toISOString();
        // Compute the closes date from the due date
        const expectedDueDate = moment(updatedDueDate)
          .add(parseInt(closesDateOffsetDays, 10), 'days')
          .toISOString();
        expect(closesDate).eq(expectedDueDate)
      })
    });
  })

  it('updates due and closes date manually, select a grading a template, but due and closes date is not updated', () => {
    const templateName = 'Template to not update date'
    const typedDueDate = 'Jun 10 05:00 PM'
    const typedClosesDate = 'Jun 22 05:00 PM'
    cy.visit('/course/2/assignment/edit/homework/new')
    cy.disableTours()
    // force update the closes date
    cy.get('input[name="tasking_plans[0].closes_at"]').clear({ force: true }).type(typedClosesDate, { force: true })
    cy.get('.oxdt-ok button').click()
    addTemplate({ name: templateName, doSelect: true })
    // after adding and selecting the template, closes date should not be updated
    cy.get('input[name="tasking_plans[0].closes_at"]').then(c => {
      const closesDate = moment(c[0].defaultValue).toISOString();
      expect(closesDate).eq(moment(typedClosesDate).toISOString())
    })
    // force update the due date
    cy.get('input[name="tasking_plans[0].due_at"]').clear({ force: true }).type(typedDueDate, { force: true })
    // After opening the closes date time picker modal, it gets the two OK buttons
    cy.get('.oxdt-ok button').last().click({ force: true })
    cy.get('[data-test-id="grading-templates"]').click()
    cy.get('[data-test-id="Default Homework"]').click()
    // after selecting the first template, due date should not be updated
    cy.get('input[name="tasking_plans[0].due_at"]').then(d => {
      const dueDate = moment(d[0].defaultValue).toISOString();
      expect(dueDate).eq(moment(typedDueDate).toISOString())
    })
  })
});
<commit_msg>remove flaky spec<commit_after>import { range } from 'lodash';
import moment from 'moment-timezone';

context('Assignment Edit', () => {
  const fillDetails = () => {
    cy.get('.heading').should('contain.text', 'STEP 1')
    cy.get('.controls .btn-primary').should('be.disabled')
    cy.get('input[name="title"]').type('test assignment #1')
    cy.get('.controls .btn-primary').should('not.be.disabled')
  }
  const addTemplate = ({ name, dueDateOffsetDays = '1', dueTimeHour = '5',
    dueTimeMinutes = '30', closesDateOffsetDays = '30', isAM = false, doSelect = false }) => {
    cy.get('[data-test-id="grading-templates"]').click()
    cy.get('[data-test-id="add-template"]').click()
    cy.get('.modal').should('be.visible')
    cy.get('.modal input[name="name"]').type(name)
    cy.get('.modal select[name="default_due_date_offset_days"]').select(dueDateOffsetDays);
    cy.get('.modal select[name="default_due_time_hour"]').select(dueTimeHour);
    cy.get('.modal select[name="default_due_time_minute"]').select(dueTimeMinutes);
    cy.get('.modal select[name="default_close_date_offset_days"]').select(closesDateOffsetDays);
    cy.get('.modal input[name="default_due_time_ampm"]').check(isAM ? 'am' : 'pm', { force: true });
    cy.get('.modal [type="submit"]').click()

    if(doSelect) {
      cy.get('[data-test-id="grading-templates"]').click()
      cy.get(`[data-test-id="${name}"]`).click()
    }
  }

  it('loads and advances homework', () => {
    cy.visit('/course/2/assignment/edit/homework/new')
    cy.disableTours();
    fillDetails()
    cy.get('.controls .btn-primary').click()
    cy.get('.heading').should('contain.text', 'STEP 2')
    cy.get('.controls .btn-primary').should('be.disabled')
    cy.get('.chapter[data-is-expanded="false"]').first().click()
    cy.get('[data-chapter-section="2.1"]').click();
    cy.get('[data-chapter-section="2.2"]').click();
    cy.get('.controls .btn-primary').should('not.be.disabled')

    cy.get('.controls .btn-primary').click()
    cy.get('.heading').should('contain.text', 'STEP 3')
    cy.get('.controls .btn-primary').should('be.disabled')
    range(3).map(i => cy.get('.exercise-card').eq(i).find('.action.include').trigger('click', { force: true }))
    cy.get('.controls .btn-primary').should('not.be.disabled')

    cy.get('.controls .btn-primary').click()
    cy.get('.heading').should('contain.text', 'STEP 4')
    cy.get('.openstax-exercise-preview').should('have.length', 3)

    cy.get('button.ox-icon-arrow-up').eq(1).click({ force: true })
  });

  it('loads and advances reading', () => {
    cy.visit('/course/1/assignment/edit/reading/new')
    cy.disableTours();
    fillDetails()
    cy.get('.controls .btn-primary').click()
    cy.get('.heading').should('contain.text', 'STEP 2')
    cy.get('.controls .btn-primary').should('be.disabled')
    cy.get('.chapter[data-is-expanded="false"]').first().click()
    cy.get('[data-chapter-section="2.1"]').click();
    cy.get('[data-chapter-section="2.2"]').click();
    cy.get('[data-chapter-section="2.3"]').click();
    cy.get('.controls .btn-primary').should('not.be.disabled')

    cy.get('.controls .btn-primary').click()
    cy.get('.heading').should('contain.text', 'STEP 3')

    cy.get('.selected-section .chapter-section').invoke('text').then(labels => {
      expect(labels).to.eq(['2.1', '2.2', '2.3'].join(''))
    })
    cy.get('button.ox-icon-arrow-up').eq(1).click()
    cy.get('.selected-section .chapter-section').invoke('text').then(labels => {
      expect(labels).to.eq(['2.1', '2.3', '2.2'].join(''))
    })
  });

  it('increases selections', () => {
    cy.visit('/course/2/assignment/edit/homework/new')
    cy.disableTours()
    fillDetails()
    cy.get('.controls .btn-primary').click()
    cy.get('.chapter-checkbox .btn').first().click()
    cy.get('.controls .btn-primary').click()
    cy.get('[data-test-id="selection-count"]').should('contain.text', '0')
    cy.get('[data-test-id="selection-count-footer"]').should('contain.text', '0')

    cy.get('.action.include:first').click({ force: true })

    cy.get('[data-test-id="selection-count"]').should('contain.text', '1')
    cy.get('[data-test-id="selection-count-footer"]').should('contain.text', '1')
    cy.get('[data-test-id="tutor-count"]').should('contain.text', '3')
    cy.get('[data-test-id="tutor-count-footer"]').should('contain.text', '3')
    cy.get('[data-test-id="total-count"]').should('contain.text', '4')
    cy.get('[data-test-id="total-count-footer"]').should('contain.text', '4')
  });

  it('filters question types', () => {
    cy.visit('/course/2/assignment/edit/homework/new')
    cy.disableTours()
    fillDetails()
    cy.get('.controls .btn-primary').click()
    cy.get('.chapter-checkbox .btn').first().click()
    cy.get('.controls .btn-primary').click()
    cy.get('[name="filter"][value="oe"]').click({ force: true })
    cy.get('[data-section]').should('not.contain.text', 'Multiple Choice Questions')
    cy.get('[name="filter"][value="mc"]').click({ force: true })
    cy.get('[data-section]').should('not.contain.text', 'Written Response Questions')
  });

  it('can change course timezone', () => {
    cy.visit('/course/2/assignment/edit/homework/new')
    cy.disableTours()
    fillDetails()
    cy.get('[data-test-id="change-timezone"]').click()
    cy.get('.settings-edit-course-modal').should('be.visible')
    cy.get('.settings-edit-course-modal [value="Hawaii"]').click({ force: true })
    cy.get('.settings-edit-course-modal .async-button').click()
    cy.get('.settings-edit-course-modal').should('not.exist')
    cy.get('[data-test-id="change-timezone"').should( 'contain.text', 'Hawaii')
  });

  it('renders external assignment', () => {
    cy.visit('/course/2/assignment/edit/external/new')
    cy.disableTours()
    cy.get('.heading').should('not.contain.text', 'STEP 1')
    cy.get('[name="settings.external_url"').type('url')
  });

  it('can add a new template', () => {
    const templateName = 'This is a new template'
    cy.visit('/course/2/assignment/edit/homework/new')
    cy.disableTours()
    fillDetails()
    addTemplate({ name: templateName })
    cy.get('[data-test-id="grading-templates"]').should('contain.text', templateName)
  });

  it('can select another template and update dates', () => {
    const templateName = 'Template to update dates'
    const dueDateOffsetDays = '3', dueTimeHour = '7', dueTimeMinutes = '15', closesDateOffsetDays = '10', isAM = false
    cy.visit('/course/2/assignment/edit/homework/new')
    cy.disableTours()
    fillDetails()
    addTemplate({ name: templateName, dueDateOffsetDays, dueTimeHour, dueTimeMinutes, closesDateOffsetDays, isAM, doSelect: true })
    cy.get('input[name="tasking_plans[0].opens_at"]').then(o => {
      const openDate = o[0].defaultValue
      let updatedDueDate;
      // Due date should change
      cy.get('input[name="tasking_plans[0].due_at"]').then(d => {
        const dueDate = moment(d[0].defaultValue).toISOString();
        updatedDueDate = dueDate;
        // Compute the due date from the open date
        const hour = isAM ? parseInt(dueTimeHour, 10) : parseInt(dueTimeHour, 10) + 12
        const expectedDueDate = moment(openDate)
          .add(parseInt(dueDateOffsetDays, 10), 'days')
          .set({ hour, minutes: parseInt(dueTimeMinutes, 10) })
          .toISOString();
        expect(dueDate).eq(expectedDueDate)
      })
      // Closes date should change
      cy.get('input[name="tasking_plans[0].closes_at"]').then(c => {
        const closesDate = moment(c[0].defaultValue).toISOString();
        // Compute the closes date from the due date
        const expectedDueDate = moment(updatedDueDate)
          .add(parseInt(closesDateOffsetDays, 10), 'days')
          .toISOString();
        expect(closesDate).eq(expectedDueDate)
      })
    });
  })
});
<|endoftext|>


<commit_before>'use strict';

const util = require('util');
const loadBalancerLib = require('../loadbalancer');
const chainMiddleware = require('../endpoint').chain;
const co = require('co');
const assertTimeout = require('co-assert-timeout');
const _ = require('lodash');
const EventEmitter = require('events').EventEmitter;
const configuration = require('../config');
const Logger = require('restore-logger');

// loadbalancers
const loadBalancers = {};

function registerLoadBalancer(name, provider) {
  loadBalancers[name] = provider;
}
module.exports.registerLoadBalancer = registerLoadBalancer;

function makeRoundRobinLB(config, publisher, logger) {
  return loadBalancerLib.roundRobin(publisher, logger);
}

function makeRandomLB(config, publisher, logger) {
  const seed = config.seed || Math.random();
  return loadBalancerLib.random(publisher, seed, logger);
}
registerLoadBalancer('roundRobin', makeRoundRobinLB);
registerLoadBalancer('random', makeRandomLB);

// publishers
const publishers = {};

/**
 * register endpoint publishers
 *
 * @param  {string} name     Publisher name
 * @param  {generator} provider generator which can be iterated
 */
function registerPublisher(name, provider) {
  publishers[name] = provider;
}
module.exports.registerPublisher = registerPublisher;
// register default publishers
function makeStaticPublisher(config, factory, logger) {
  return loadBalancerLib.staticPublisher(config.instances, factory, logger);
}
registerPublisher('static', makeStaticPublisher);

// transport providers
const transportProviders = {};

/**
 * register a transport
 *
 * @param  {string} name      transport identifier
 * @param  {constructor} transport Transport provider constructor
 */
function registerTransport(name, transport) {
  transportProviders[name] = transport;
}
module.exports.registerTransport = registerTransport;
// register default transport providers
const grpc = require('../transport/provider/grpc');
registerTransport(grpc.Name, grpc.Client);

util.inherits(Client, EventEmitter);

/**
 * Client is a microservice client.
 *
 * @param {Object} name Name of the configured client.
 */
function Client(name, config, logger) {
  EventEmitter.call(this);

  /* eslint consistent-this: ["error", "that"]*/
  const that = this;

  if (_.isNil(config)) {
    const cfg = configuration.get(logger);
    if (!cfg) {
      throw new Error('no configuration found');
    }
    this.$config = cfg.get(util.format('client:%s', name));
    if (!this.$config) {
      throw new Error(util.format('no client:%s config', name));
    }
  } else {
    this.$config = config;
  }
  // check config
  if (!this.$config.endpoints || _.keys(this.$config.endpoints).length === 0) {
    throw new Error('no endpoints configured');
  }
  if (!this.$config.transports || _.keys(this.$config.transports).length === 0) {
    throw new Error('no transports configured');
  }

  if (_.isNil(logger)) {
    // logger
    if (!this.$config.logger) {
      this.$config.logger = {
        console: {
          handleExceptions: false,
          level: 'silly',
          colorize: true,
          prettyPrint: true,
        },
      };
    }
    const loggerCfg = {
      value: this.$config.logger,
      get() {
        return this.value;
      },
    };
    this.logger = new Logger(loggerCfg);
  } else {
    this.logger = logger;
  }
  // transport
  const transports = [];
  _.forIn(this.$config.transports, (transportConfig, transportName) => {
    const Transport = transportProviders[transportName];
    if (!Transport) {
      that.logger.error(
        util.format('transport %s does not exist', transportName));
      return;
    }
    try {
      const provider = new Transport(transportConfig, that.logger);
      transports.push(provider);
    } catch (e) {
      that.logger.error(e.stack);
    }
  });
  if (transports.length === 0) {
    throw new Error('no transports properly configured');
  }
  this.$transports = transports;

  // detect global loadbalancer
  let defaultLoadBalancer = loadBalancers.roundRobin;
  if (this.$config.loadbalancer) {
    defaultLoadBalancer = loadBalancers[this.$config.loadbalancer.name];
  }

  // detect global publisher
  let defaultPublisher;
  if (this.$config.publisher) {
    defaultPublisher = publishers[this.$config.publisher.name];
    if (!defaultPublisher) {
      this.logger.error(
        util.format('publisher %s does not exist', this.$config.publisher.name));
    }
  }

  // setup endpoints
  this.$endpoints = {};
  _.forIn(this.$config.endpoints, (endpointConfig, endpointName) => {
    // publisher
    let publisher = defaultPublisher;
    let publisherCfg = this.$config.publisher;
    if (endpointConfig.publisher && endpointConfig.publisher.name) {
      publisher = publishers[endpointConfig.publisher.name];
      publisherCfg = endpointConfig.publisher;
    }
    if (!publisher) {
      if (!publisherCfg) {
        this.logger.error(
          util.format('publisher configuration for endpoint %s does not exist',
            endpointName));
        return;
      }
      this.logger.error(
        util.format('publisher %s does not exist', publisherCfg.name));
      return;
    }

    // loadBalancer
    let loadBalancer = defaultLoadBalancer;
    let loadBalancerCfg = this.$config.loadbalancer;
    if (endpointConfig.loadbalancer && endpointConfig.loadbalancer.name) {
      loadBalancer = loadBalancers[endpointConfig.loadbalancer.name];
      loadBalancerCfg = endpointConfig.loadbalancer;
    }
    if (!loadBalancer) {
      if (!loadBalancerCfg) {
        this.logger.error(
          util.format('loadBalancer endpoint configuration %s does not exist',
            endpointName));
        return;
      }
      this.logger.error(
        util.format('loadbalancer %s does not exist', loadBalancerCfg.name));
      return;
    }

    this.$endpoints[endpointName] = {
      publisher, // publisher(config, factory)
      publisherConfig: publisherCfg,
      loadBalancer, // loadBalancer(config, publisher)
      loadBalancerConfig: loadBalancerCfg,
    };
  });
  if (Object.keys(this.$endpoints).length === 0) {
    throw new Error('no endpoints properly configured');
  }
}

/**
 * A list of middleware which gets called before the endpoint.
 * The endpoint includes [retry, timeout], publisher, loadBalancer, transport.
 *
 * @type {Array}
 */
Client.prototype.middleware = [];

function* getEndpoint(loadBalancer) {
  return yield(co(function* getEndpointFromLB() {
    const lb = loadBalancer.next();
    if (lb.done) {
      throw new Error('no endpoints');
    }
    return lb.value;
  }).catch((err) => {
    throw err;
  }));
}

// handles retries, timeout, middleware, calling the loadBalancer and errors
function makeServiceEndpoint(name, middleware, loadBalancer, logger) {
  const e = function* handleRetryAndMiddleware(request, options) {
    if (!request) {
      throw new ReferenceError('request is undefined');
    }
    // FIXME This generator is called in a loop when gRPC does not call the callback function
    // This happens if the gRPC error code is:
    // INTERNAL and some other codes which indicate a server error, not an input error
    let attempts = 1;
    if (options && options.retry) {
      attempts += options.retry;
    }
    const errs = [];
    const context = Object.assign(options || {}, {
      endpointName: name,
      attempts,
      currentAttempt: 1,
    });
    logger.debug(
      util.format('calling endpoint with request %j', request));
    for (let i = 1; i <= attempts; i++) {
      context.currentAttempt = i;
      logger.debug(
        util.format('attempt %d/%d calling endpoint with request %j',
          i, attempts, request));
      try {
        let endpoint = yield getEndpoint(loadBalancer);
        if (middleware.length !== 0) {
          const chain = chainMiddleware(middleware);
          endpoint = yield chain(endpoint);
        }
        const result = yield endpoint(request, context);
        if (result.error) {
          switch (result.error.message) {
            case 'unimplemented':
            case 'resource exhausted':
            case 'unknown':
            case 'internal':
            case 'unavailable':
            case 'data loss':
              logger.error(
                util.format('attempt %d/%d error: %s',
                  i, attempts, result.error));
              errs.push(result.error);
              // retry
              continue;
            default:
              return yield result;
          }
        }
        return yield result;
      } catch (err) {
        logger.error(
          util.format('attempt %d/%d error: %s', i, attempts, err.stack));
        errs.push(err);
        if (err.message === 'call timeout') {
          logger.debug(
            util.format('attempt %d/%d returning with call timeout',
              i, attempts));
          return {
            error: errs,
          };
        }
      }
    }
    return {
      error: errs,
    };
  };
  return function* handleTimeout(req, options) {
    if (options && options.timeout) {
      const gen = e(req, options);
      return yield co(function* checkTimeout() {
        return yield assertTimeout(gen, options.timeout);
      }).catch((error) => {
        if (error.status === 408) {
          const err = new Error('call timeout');
          const res = gen.throw(err);
          return res.value;
        }
        return {
          error,
        };
      });
    }
    return yield co(function* callEndpoint() {
      return yield e(req, options);
    }).catch((err) => {
      return {
        error: err,
      };
    });
  };
}

// returns a factory which turns an instance into an endpoint via a transport provider
function generalFactory(method, transports, logger) {
  return function* makeEndpoints(instance) {
    for (let i = 0; i < transports.length; i++) {
      try {
        const endpoint = yield* transports[i].makeEndpoint(method, instance);
        return endpoint;
      } catch (e) {
        logger.debug('generalFactory transport.makeEndpoint',
          method, transports[i].name, instance, e);
      }
    }
    throw new Error('no endpoint');
  };
}

/**
 * Connect to the provided endpoints via specified transports.
 *
 * @return {Object} Service with endpoint methods.
 */
Client.prototype.connect = function* connect() {
  /* eslint consistent-this: ["error", "that"]*/
  const that = this;

  const s = yield co(function* createService() {
    const service = {};
    _.forIn(that.$endpoints, (e, name) => {
      const factory = generalFactory(name, that.$transports, that.logger);
      const publisher = e.publisher(e.publisherConfig, factory, that.logger);
      const loadBalancer = e.loadBalancer(e.loadBalancerConfig,
        publisher, that.logger);
      service[name] = makeServiceEndpoint(name,
        that.middleware, loadBalancer, that.logger);
    });
    that.logger.verbose('gRPC service ready', service);
    return service;
  });
  this.emit('connected', s);
  return s;
};

Client.prototype.end = function* end() {
  for (let i = 0; i < this.$transports.length; i++) {
    const transport = this.$transports[i];
    yield transport.end();
    this.emit('disconnected', transport);
  }
};

module.exports.Client = Client;
<commit_msg>Allow calls without a request<commit_after>'use strict';

const util = require('util');
const loadBalancerLib = require('../loadbalancer');
const chainMiddleware = require('../endpoint').chain;
const co = require('co');
const assertTimeout = require('co-assert-timeout');
const _ = require('lodash');
const EventEmitter = require('events').EventEmitter;
const configuration = require('../config');
const Logger = require('restore-logger');

// loadbalancers
const loadBalancers = {};

function registerLoadBalancer(name, provider) {
  loadBalancers[name] = provider;
}
module.exports.registerLoadBalancer = registerLoadBalancer;

function makeRoundRobinLB(config, publisher, logger) {
  return loadBalancerLib.roundRobin(publisher, logger);
}

function makeRandomLB(config, publisher, logger) {
  const seed = config.seed || Math.random();
  return loadBalancerLib.random(publisher, seed, logger);
}
registerLoadBalancer('roundRobin', makeRoundRobinLB);
registerLoadBalancer('random', makeRandomLB);

// publishers
const publishers = {};

/**
 * register endpoint publishers
 *
 * @param  {string} name     Publisher name
 * @param  {generator} provider generator which can be iterated
 */
function registerPublisher(name, provider) {
  publishers[name] = provider;
}
module.exports.registerPublisher = registerPublisher;
// register default publishers
function makeStaticPublisher(config, factory, logger) {
  return loadBalancerLib.staticPublisher(config.instances, factory, logger);
}
registerPublisher('static', makeStaticPublisher);

// transport providers
const transportProviders = {};

/**
 * register a transport
 *
 * @param  {string} name      transport identifier
 * @param  {constructor} transport Transport provider constructor
 */
function registerTransport(name, transport) {
  transportProviders[name] = transport;
}
module.exports.registerTransport = registerTransport;
// register default transport providers
const grpc = require('../transport/provider/grpc');
registerTransport(grpc.Name, grpc.Client);

util.inherits(Client, EventEmitter);

/**
 * Client is a microservice client.
 *
 * @param {Object} name Name of the configured client.
 */
function Client(name, config, logger) {
  EventEmitter.call(this);

  /* eslint consistent-this: ["error", "that"]*/
  const that = this;

  if (_.isNil(config)) {
    const cfg = configuration.get(logger);
    if (!cfg) {
      throw new Error('no configuration found');
    }
    this.$config = cfg.get(util.format('client:%s', name));
    if (!this.$config) {
      throw new Error(util.format('no client:%s config', name));
    }
  } else {
    this.$config = config;
  }
  // check config
  if (!this.$config.endpoints || _.keys(this.$config.endpoints).length === 0) {
    throw new Error('no endpoints configured');
  }
  if (!this.$config.transports || _.keys(this.$config.transports).length === 0) {
    throw new Error('no transports configured');
  }

  if (_.isNil(logger)) {
    // logger
    if (!this.$config.logger) {
      this.$config.logger = {
        console: {
          handleExceptions: false,
          level: 'silly',
          colorize: true,
          prettyPrint: true,
        },
      };
    }
    const loggerCfg = {
      value: this.$config.logger,
      get() {
        return this.value;
      },
    };
    this.logger = new Logger(loggerCfg);
  } else {
    this.logger = logger;
  }
  // transport
  const transports = [];
  _.forIn(this.$config.transports, (transportConfig, transportName) => {
    const Transport = transportProviders[transportName];
    if (!Transport) {
      that.logger.error(
        util.format('transport %s does not exist', transportName));
      return;
    }
    try {
      const provider = new Transport(transportConfig, that.logger);
      transports.push(provider);
    } catch (e) {
      that.logger.error(e.stack);
    }
  });
  if (transports.length === 0) {
    throw new Error('no transports properly configured');
  }
  this.$transports = transports;

  // detect global loadbalancer
  let defaultLoadBalancer = loadBalancers.roundRobin;
  if (this.$config.loadbalancer) {
    defaultLoadBalancer = loadBalancers[this.$config.loadbalancer.name];
  }

  // detect global publisher
  let defaultPublisher;
  if (this.$config.publisher) {
    defaultPublisher = publishers[this.$config.publisher.name];
    if (!defaultPublisher) {
      this.logger.error(
        util.format('publisher %s does not exist', this.$config.publisher.name));
    }
  }

  // setup endpoints
  this.$endpoints = {};
  _.forIn(this.$config.endpoints, (endpointConfig, endpointName) => {
    // publisher
    let publisher = defaultPublisher;
    let publisherCfg = this.$config.publisher;
    if (endpointConfig.publisher && endpointConfig.publisher.name) {
      publisher = publishers[endpointConfig.publisher.name];
      publisherCfg = endpointConfig.publisher;
    }
    if (!publisher) {
      if (!publisherCfg) {
        this.logger.error(
          util.format('publisher configuration for endpoint %s does not exist',
            endpointName));
        return;
      }
      this.logger.error(
        util.format('publisher %s does not exist', publisherCfg.name));
      return;
    }

    // loadBalancer
    let loadBalancer = defaultLoadBalancer;
    let loadBalancerCfg = this.$config.loadbalancer;
    if (endpointConfig.loadbalancer && endpointConfig.loadbalancer.name) {
      loadBalancer = loadBalancers[endpointConfig.loadbalancer.name];
      loadBalancerCfg = endpointConfig.loadbalancer;
    }
    if (!loadBalancer) {
      if (!loadBalancerCfg) {
        this.logger.error(
          util.format('loadBalancer endpoint configuration %s does not exist',
            endpointName));
        return;
      }
      this.logger.error(
        util.format('loadbalancer %s does not exist', loadBalancerCfg.name));
      return;
    }

    this.$endpoints[endpointName] = {
      publisher, // publisher(config, factory)
      publisherConfig: publisherCfg,
      loadBalancer, // loadBalancer(config, publisher)
      loadBalancerConfig: loadBalancerCfg,
    };
  });
  if (Object.keys(this.$endpoints).length === 0) {
    throw new Error('no endpoints properly configured');
  }
}

/**
 * A list of middleware which gets called before the endpoint.
 * The endpoint includes [retry, timeout], publisher, loadBalancer, transport.
 *
 * @type {Array}
 */
Client.prototype.middleware = [];

function* getEndpoint(loadBalancer) {
  return yield(co(function* getEndpointFromLB() {
    const lb = loadBalancer.next();
    if (lb.done) {
      throw new Error('no endpoints');
    }
    return lb.value;
  }).catch((err) => {
    throw err;
  }));
}

// handles retries, timeout, middleware, calling the loadBalancer and errors
function makeServiceEndpoint(name, middleware, loadBalancer, logger) {
  const e = function* handleRetryAndMiddleware(request, options) {
    let attempts = 1;
    if (options && options.retry) {
      attempts += options.retry;
    }
    const errs = [];
    const context = Object.assign(options || {}, {
      endpointName: name,
      attempts,
      currentAttempt: 1,
    });
    logger.debug(
      util.format('calling endpoint with request %j', request));
    for (let i = 1; i <= attempts; i++) {
      context.currentAttempt = i;
      logger.debug(
        util.format('attempt %d/%d calling endpoint with request %j',
          i, attempts, request));
      try {
        let endpoint = yield getEndpoint(loadBalancer);
        if (middleware.length !== 0) {
          const chain = chainMiddleware(middleware);
          endpoint = yield chain(endpoint);
        }
        const result = yield endpoint(request, context);
        if (result.write || result.read) {
          return result;
        }
        if (result.error) {
          switch (result.error.message) {
            case 'unimplemented':
            case 'resource exhausted':
            case 'unknown':
            case 'internal':
            case 'unavailable':
            case 'data loss':
              logger.error(
                util.format('attempt %d/%d error: %s',
                  i, attempts, result.error));
              errs.push(result.error);
              // retry
              continue;
            default:
              return yield result;
          }
        }
        return yield result;
      } catch (err) {
        logger.error(
          util.format('attempt %d/%d error: %s', i, attempts, err.stack));
        errs.push(err);
        if (err.message === 'call timeout') {
          logger.debug(
            util.format('attempt %d/%d returning with call timeout',
              i, attempts));
          return {
            error: errs,
          };
        }
      }
    }
    return {
      error: errs,
    };
  };
  return function* handleTimeout(req, options) {
    if (options && options.timeout) {
      const gen = e(req, options);
      return yield co(function* checkTimeout() {
        return yield assertTimeout(gen, options.timeout);
      }).catch((error) => {
        if (error.status === 408) {
          const err = new Error('call timeout');
          const res = gen.throw(err);
          return res.value;
        }
        return {
          error,
        };
      });
    }
    return yield co(function* callEndpoint() {
      return yield e(req, options);
    }).catch((err) => {
      return {
        error: err,
      };
    });
  };
}

// returns a factory which turns an instance into an endpoint via a transport provider
function generalFactory(method, transports, logger) {
  return function* makeEndpoints(instance) {
    for (let i = 0; i < transports.length; i++) {
      try {
        const endpoint = yield* transports[i].makeEndpoint(method, instance);
        return endpoint;
      } catch (e) {
        logger.debug('generalFactory transport.makeEndpoint',
          method, transports[i].name, instance, e);
      }
    }
    throw new Error('no endpoint');
  };
}

/**
 * Connect to the provided endpoints via specified transports.
 *
 * @return {Object} Service with endpoint methods.
 */
Client.prototype.connect = function* connect() {
  /* eslint consistent-this: ["error", "that"]*/
  const that = this;

  const s = yield co(function* createService() {
    const service = {};
    _.forIn(that.$endpoints, (e, name) => {
      const factory = generalFactory(name, that.$transports, that.logger);
      const publisher = e.publisher(e.publisherConfig, factory, that.logger);
      const loadBalancer = e.loadBalancer(e.loadBalancerConfig,
        publisher, that.logger);
      service[name] = makeServiceEndpoint(name,
        that.middleware, loadBalancer, that.logger);
    });
    that.logger.verbose('gRPC service ready', service);
    return service;
  });
  this.emit('connected', s);
  return s;
};

Client.prototype.end = function* end() {
  for (let i = 0; i < this.$transports.length; i++) {
    const transport = this.$transports[i];
    yield transport.end();
    this.emit('disconnected', transport);
  }
};

module.exports.Client = Client;
<|endoftext|>


<commit_before>'use strict';

const MailosaurError = require('../models/mailosaurError');
const Message = require('../models/message');
const MessageListResult = require('../models/messageListResult');

/** Class representing Messages operations. */
class Messages {
  /**
   * Create a Messages.
   * @param {MailosaurClient} client Reference to the Mailosaur client.
   */
  constructor(client) {
    this.client = client;
  }

  /**
   * @summary Retrieve a message using search criteria
   *
   * Returns as soon as a message matching the specified search criteria is
   * found. This is the most efficient method of looking up a message.
   *
   * @param {string} server The identifier of the server hosting the message.
   *
   * @param {object} criteria The search criteria to use in order to find a
   * match.
   *
   * @param {string} [criteria.sentTo] The full email address to which the target
   * email was sent.
   *
   * @param {string} [criteria.subject] The value to seek within the target
   * email's subject line.
   *
   * @param {string} [criteria.body] The value to seek within the target email's
   * HTML or text body.
   *
   * @param {object} [options] Optional Parameters.
   *
   * @param {number} [options.timeout] Specify how long to wait for a matching
   * result (in milliseconds).
   *
   * @param {date} [options.receivedAfter] Limits results to only messages
   * received after this date/time (default 20 seconds ago).
   *
   * @param {function} [optionalCallback] - The optional callback.
   *
   * @returns {function|Promise} If a callback was passed as the last parameter
   * then it returns the callback else returns a Promise.
   *
   * {Promise} A promise is returned
   *
   *                      @resolve {Message} - The deserialized result object.
   *
   *                      @reject {Error} - The error object.
   *
   * {function} optionalCallback(err, result, request, response)
   *
   *                      {Error}  err        - The Error object if an error occurred, null otherwise.
   *
   *                      {object} [result]   - The deserialized result object if an error did not occur.
   *                      See {@link Message} for more information.
   */
  get(server, criteria, options, optionalCallback) {
    let self = this;
    let getByIdError = new Error('Use getById to retrieve a message using its identifier');

    options = options || {};

    // Default timeout to 10s
    options.timeout = options.timeout || 10000;

    // Default receivedAfter to 1h
    options.receivedAfter = options.receivedAfter || new Date(Date.now() - 3600000);

    if (server.length > 8) {
      return optionalCallback ? optionalCallback(getByIdError) : new Promise((resolve, reject) => {
        reject(getByIdError);
      });
    }

    if (!optionalCallback) {
      return new Promise((resolve, reject) => {
        self.search(server, criteria, options).then((result) => {
          return self.getById(result.items[0].id);
        }).then((message) => {
          resolve(message);
        }).catch(reject);
      });
    } else {
      self.search(server, criteria, options).then((result) => {
        self.getById(result.items[0].id, optionalCallback);
      });
    }
  }

  /**
   * @summary Retrieve a message using message id
   *
   * Retrieves the detail for a single email message. Simply supply the unique
   * identifier for the required message.
   *
   * @param {uuid} id The identifier of the email message to be retrieved.
   *
   * @param {function} [optionalCallback] - The optional callback.
   *
   * @returns {function|Promise} If a callback was passed as the last parameter
   * then it returns the callback else returns a Promise.
   *
   * {Promise} A promise is returned
   *
   *                      @resolve {Message} - The deserialized result object.
   *
   *                      @reject {Error} - The error object.
   *
   * {function} optionalCallback(err, result, request, response)
   *
   *                      {Error}  err        - The Error object if an error occurred, null otherwise.
   *
   *                      {object} [result]   - The deserialized result object if an error did not occur.
   *                      See {@link Message} for more information.
   */
  getById(id, optionalCallback) {
    let client = this.client;
    let self = this;
    let url = `api/messages/${id}`;

    if (!optionalCallback) {
      return new Promise((resolve, reject) => {
        self.client.request.get(url, (err, response, body) => {
          if (err || response.statusCode !== 200) {
            return reject(new MailosaurError(response));
          }
          resolve(new Message(body));
        });
      });
    } else {
      self.client.request.get(url, (err, response, body) => {
        if (err || response.statusCode !== 200) {
          return optionalCallback(new MailosaurError(response));
        }
        optionalCallback(null, new Message(body));
      });
    }
  }

  /**
   * @summary Delete a message
   *
   * Permanently deletes a message. This operation cannot be undone. Also deletes
   * any attachments related to the message.
   *
   * @param {uuid} id The identifier of the message to be deleted.
   *
   * @param {function} [optionalCallback] - The optional callback.
   *
   * @returns {function|Promise} If a callback was passed as the last parameter
   * then it returns the callback else returns a Promise.
   *
   * {Promise} A promise is returned
   *
   *                      @resolve {null} - The deserialized result object.
   *
   *                      @reject {Error} - The error object.
   *
   * {function} optionalCallback(err, result, request, response)
   *
   *                      {Error}  err        - The Error object if an error occurred, null otherwise.
   *
   *                      {null} [result]   - The deserialized result object if an error did not occur.
   */
  del(id, optionalCallback) {
    let client = this.client;
    let self = this;
    let url = `api/messages/${id}`;

    if (!optionalCallback) {
      return new Promise((resolve, reject) => {
        self.client.request.del(url, (err, response, body) => {
          if (err || response.statusCode !== 204) {
            return reject(new MailosaurError(response));
          }
          resolve();
        });
      });
    } else {
      self.client.request.del(url, (err, response, body) => {
        if (err || response.statusCode !== 204) {
          return optionalCallback(new MailosaurError(response));
        }
        optionalCallback(null);
      });
    }
  }

  /**
   * @summary List all messages
   *
   * Returns a list of your messages in summary form. The summaries are returned
   * sorted by received date, with the most recently-received messages appearing
   * first.
   *
   * @param {string} server The identifier of the server hosting the messages.
   *
   * @param {object} [options] Optional Parameters.
   *
   * @param {number} [options.page] Used in conjunction with `itemsPerPage` to
   * support pagination.
   *
   * @param {number} [options.itemsPerPage] A limit on the number of results to
   * be returned per page. Can be set between 1 and 1000 items, the default is
   * 50.
   *
   * @param {function} [optionalCallback] - The optional callback.
   *
   * @returns {function|Promise} If a callback was passed as the last parameter
   * then it returns the callback else returns a Promise.
   *
   * {Promise} A promise is returned
   *
   *                      @resolve {MessageListResult} - The deserialized result object.
   *
   *                      @reject {Error} - The error object.
   *
   * {function} optionalCallback(err, result, request, response)
   *
   *                      {Error}  err        - The Error object if an error occurred, null otherwise.
   *
   *                      {object} [result]   - The deserialized result object if an error did not occur.
   *                      See {@link MessageListResult} for more information.
   */
  list(server, options, optionalCallback) {
    let client = this.client;
    let self = this;
    let url = `api/messages`;

    options = options || {};
    let qs = {
      server: server,
      page: options.page,
      itemsPerPage: options.itemsPerPage
    };

    if (!optionalCallback) {
      return new Promise((resolve, reject) => {
        self.client.request.get(url, { qs: qs }, (err, response, body) => {
          if (err || response.statusCode !== 200) {
            return reject(new MailosaurError(response));
          }
          resolve(new MessageListResult(body));
        });
      });
    } else {
      self.client.request.get(url, { qs: qs }, (err, response, body) => {
        if (err || response.statusCode !== 200) {
          return optionalCallback(new MailosaurError(response));
        }
        optionalCallback(null, new MessageListResult(body));
      });
    }
  }

  /**
   * @summary Delete all messages
   *
   * Permanently deletes all messages held by the specified server. This
   * operation cannot be undone. Also deletes any attachments related to each
   * message.
   *
   * @param {string} server The identifier of the server to be emptied.
   *
   * @param {function} [optionalCallback] - The optional callback.
   *
   * @returns {function|Promise} If a callback was passed as the last parameter
   * then it returns the callback else returns a Promise.
   *
   * {Promise} A promise is returned
   *
   *                      @resolve {null} - The deserialized result object.
   *
   *                      @reject {Error} - The error object.
   *
   * {function} optionalCallback(err, result, request, response)
   *
   *                      {Error}  err        - The Error object if an error occurred, null otherwise.
   *
   *                      {null} [result]   - The deserialized result object if an error did not occur.
   */
  deleteAll(server, optionalCallback) {
    let client = this.client;
    let self = this;
    let url = `api/messages`;

    let qs = {
      server: server
    };

    if (!optionalCallback) {
      return new Promise((resolve, reject) => {
        self.client.request.del(url, { qs: qs }, (err, response, body) => {
          if (err || response.statusCode !== 204) {
            return reject(new MailosaurError(response));
          }
          resolve();
        });
      });
    } else {
      self.client.request.del(url, { qs: qs }, (err, response, body) => {
        if (err || response.statusCode !== 204) {
          return optionalCallback(new MailosaurError(response));
        }
        optionalCallback(null);
      });
    }
  }

  /**
   * @summary Search for messages
   *
   * Returns a list of messages matching the specified search criteria, in
   * summary form. The messages are returned sorted by received date, with the
   * most recently-received messages appearing first.
   *
   * @param {string} server The identifier of the server hosting the messages.
   *
   * @param {object} criteria The search criteria to match results against.
   *
   * @param {string} [criteria.sentTo] The full email address to which the target
   * email was sent.
   *
   * @param {string} [criteria.subject] The value to seek within the target
   * email's subject line.
   *
   * @param {string} [criteria.body] The value to seek within the target email's
   * HTML or text body.
   *
   * @param {object} [options] Optional Parameters.
   *
   * @param {number} [options.page] Used in conjunction with `itemsPerPage` to
   * support pagination.
   *
   * @param {number} [options.itemsPerPage] A limit on the number of results to
   * be returned per page. Can be set between 1 and 1000 items, the default is
   * 50.
   *
   * @param {number} [options.timeout] Specify how long to wait for a matching
   * result (in milliseconds).
   *
   * @param {date} [options.receivedAfter] Limits results to only messages
   * received after this date/time.
   *
   * @param {function} [optionalCallback] - The optional callback.
   *
   * @returns {function|Promise} If a callback was passed as the last parameter
   * then it returns the callback else returns a Promise.
   *
   * {Promise} A promise is returned
   *
   *                      @resolve {MessageListResult} - The deserialized result object.
   *
   *                      @reject {Error} - The error object.
   *
   * {function} optionalCallback(err, result, request, response)
   *
   *                      {Error}  err        - The Error object if an error occurred, null otherwise.
   *
   *                      {object} [result]   - The deserialized result object if an error did not occur.
   *                      See {@link MessageListResult} for more information.
   */
  search(server, criteria, options, optionalCallback) {
    let client = this.client;
    let self = this;
    let url = `api/messages/search`;
    let pollCount = 0;
    let startTime = Date.now();

    options = options || {};
    let qs = {
      server: server,
      page: options.page,
      itemPerPage: options.itemsPerPage,
      receivedAfter: options.receivedAfter
    };

    if (!Number.isInteger(options.timeout)) {
      options.timeout = 0;
    }

    if (!optionalCallback) {
      let fn = (resolve, reject) => function() {
        self.client.request.post(url, { qs: qs, body: criteria }, (err, response, body) => {
          if (err || response.statusCode !== 200) {
              return reject(new MailosaurError(response));
          }

          if (options.timeout && !body.items.length) {
            let delayPattern = (response.headers['x-ms-delay'] || '1000')
              .split(',')
              .map(x => parseInt(x, 10));

            let delay = (pollCount >= delayPattern.length) ?
              delayPattern[delayPattern.length - 1] :
              delayPattern[pollCount];

            pollCount++;

            // Stop if timeout will be exceeded
            if (((Date.now() - startTime) + delay) > options.timeout) {
              return reject(new MailosaurError(response));
            }

            return setTimeout(fn(resolve, reject), delay);
          }

          resolve(new MessageListResult(body));
        });
      };

      return new Promise((resolve, reject) => {
        fn(resolve, reject)();
      });
    } else {
      let fn = () => {
        self.client.request.post(url, { qs: qs, body: criteria }, (err, response, body) => {
          if (err || response.statusCode !== 200) {
            return optionalCallback(new MailosaurError(response));
          }

          if (options.timeout && !body.items.length) {
            let delayPattern = (response.headers['x-ms-delay'] || '1000')
              .split(',')
              .map(x => parseInt(x, 10));

            let delay = (pollCount >= delayPattern.length) ?
              delayPattern[delayPattern.length - 1] :
              delayPattern[pollCount];

            pollCount++;

            // Stop if timeout will be exceeded
            if (((Date.now() - startTime) + delay) > options.timeout) {
              return optionalCallback(new MailosaurError(response));
            }

            return setTimeout(fn(resolve, reject), delay);
          }

          optionalCallback(null, new MessageListResult(body));
        });
      };

      fn();
    }
  }
}

module.exports = Messages;
<commit_msg>fix for #21<commit_after>'use strict';

const MailosaurError = require('../models/mailosaurError');
const Message = require('../models/message');
const MessageListResult = require('../models/messageListResult');

/** Class representing Messages operations. */
class Messages {
  /**
   * Create a Messages.
   * @param {MailosaurClient} client Reference to the Mailosaur client.
   */
  constructor(client) {
    this.client = client;
  }

  /**
   * @summary Retrieve a message using search criteria
   *
   * Returns as soon as a message matching the specified search criteria is
   * found. This is the most efficient method of looking up a message.
   *
   * @param {string} server The identifier of the server hosting the message.
   *
   * @param {object} criteria The search criteria to use in order to find a
   * match.
   *
   * @param {string} [criteria.sentTo] The full email address to which the target
   * email was sent.
   *
   * @param {string} [criteria.subject] The value to seek within the target
   * email's subject line.
   *
   * @param {string} [criteria.body] The value to seek within the target email's
   * HTML or text body.
   *
   * @param {object} [options] Optional Parameters.
   *
   * @param {number} [options.timeout] Specify how long to wait for a matching
   * result (in milliseconds).
   *
   * @param {date} [options.receivedAfter] Limits results to only messages
   * received after this date/time (default 20 seconds ago).
   *
   * @param {function} [optionalCallback] - The optional callback.
   *
   * @returns {function|Promise} If a callback was passed as the last parameter
   * then it returns the callback else returns a Promise.
   *
   * {Promise} A promise is returned
   *
   *                      @resolve {Message} - The deserialized result object.
   *
   *                      @reject {Error} - The error object.
   *
   * {function} optionalCallback(err, result, request, response)
   *
   *                      {Error}  err        - The Error object if an error occurred, null otherwise.
   *
   *                      {object} [result]   - The deserialized result object if an error did not occur.
   *                      See {@link Message} for more information.
   */
  get(server, criteria, options, optionalCallback) {
    let self = this;
    let getByIdError = new Error('Use getById to retrieve a message using its identifier');

    options = options || {};

    // Default timeout to 10s
    options.timeout = options.timeout || 10000;

    // Default receivedAfter to 1h
    options.receivedAfter = options.receivedAfter || new Date(Date.now() - 3600000);

    if (server.length > 8) {
      return optionalCallback ? optionalCallback(getByIdError) : new Promise((resolve, reject) => {
        reject(getByIdError);
      });
    }

    if (!optionalCallback) {
      return new Promise((resolve, reject) => {
        self.search(server, criteria, options).then((result) => {
          return self.getById(result.items[0].id);
        }).then((message) => {
          resolve(message);
        }).catch(reject);
      });
    } else {
      self.search(server, criteria, options).then((result) => {
        self.getById(result.items[0].id, optionalCallback);
      });
    }
  }

  /**
   * @summary Retrieve a message using message id
   *
   * Retrieves the detail for a single email message. Simply supply the unique
   * identifier for the required message.
   *
   * @param {uuid} id The identifier of the email message to be retrieved.
   *
   * @param {function} [optionalCallback] - The optional callback.
   *
   * @returns {function|Promise} If a callback was passed as the last parameter
   * then it returns the callback else returns a Promise.
   *
   * {Promise} A promise is returned
   *
   *                      @resolve {Message} - The deserialized result object.
   *
   *                      @reject {Error} - The error object.
   *
   * {function} optionalCallback(err, result, request, response)
   *
   *                      {Error}  err        - The Error object if an error occurred, null otherwise.
   *
   *                      {object} [result]   - The deserialized result object if an error did not occur.
   *                      See {@link Message} for more information.
   */
  getById(id, optionalCallback) {
    let client = this.client;
    let self = this;
    let url = `api/messages/${id}`;

    if (!optionalCallback) {
      return new Promise((resolve, reject) => {
        self.client.request.get(url, (err, response, body) => {
          if (err || response.statusCode !== 200) {
            return reject(new MailosaurError(response));
          }
          resolve(new Message(body));
        });
      });
    } else {
      self.client.request.get(url, (err, response, body) => {
        if (err || response.statusCode !== 200) {
          return optionalCallback(new MailosaurError(response));
        }
        optionalCallback(null, new Message(body));
      });
    }
  }

  /**
   * @summary Delete a message
   *
   * Permanently deletes a message. This operation cannot be undone. Also deletes
   * any attachments related to the message.
   *
   * @param {uuid} id The identifier of the message to be deleted.
   *
   * @param {function} [optionalCallback] - The optional callback.
   *
   * @returns {function|Promise} If a callback was passed as the last parameter
   * then it returns the callback else returns a Promise.
   *
   * {Promise} A promise is returned
   *
   *                      @resolve {null} - The deserialized result object.
   *
   *                      @reject {Error} - The error object.
   *
   * {function} optionalCallback(err, result, request, response)
   *
   *                      {Error}  err        - The Error object if an error occurred, null otherwise.
   *
   *                      {null} [result]   - The deserialized result object if an error did not occur.
   */
  del(id, optionalCallback) {
    let client = this.client;
    let self = this;
    let url = `api/messages/${id}`;

    if (!optionalCallback) {
      return new Promise((resolve, reject) => {
        self.client.request.del(url, (err, response, body) => {
          if (err || response.statusCode !== 204) {
            return reject(new MailosaurError(response));
          }
          resolve();
        });
      });
    } else {
      self.client.request.del(url, (err, response, body) => {
        if (err || response.statusCode !== 204) {
          return optionalCallback(new MailosaurError(response));
        }
        optionalCallback(null);
      });
    }
  }

  /**
   * @summary List all messages
   *
   * Returns a list of your messages in summary form. The summaries are returned
   * sorted by received date, with the most recently-received messages appearing
   * first.
   *
   * @param {string} server The identifier of the server hosting the messages.
   *
   * @param {object} [options] Optional Parameters.
   *
   * @param {number} [options.page] Used in conjunction with `itemsPerPage` to
   * support pagination.
   *
   * @param {number} [options.itemsPerPage] A limit on the number of results to
   * be returned per page. Can be set between 1 and 1000 items, the default is
   * 50.
   *
   * @param {function} [optionalCallback] - The optional callback.
   *
   * @returns {function|Promise} If a callback was passed as the last parameter
   * then it returns the callback else returns a Promise.
   *
   * {Promise} A promise is returned
   *
   *                      @resolve {MessageListResult} - The deserialized result object.
   *
   *                      @reject {Error} - The error object.
   *
   * {function} optionalCallback(err, result, request, response)
   *
   *                      {Error}  err        - The Error object if an error occurred, null otherwise.
   *
   *                      {object} [result]   - The deserialized result object if an error did not occur.
   *                      See {@link MessageListResult} for more information.
   */
  list(server, options, optionalCallback) {
    let client = this.client;
    let self = this;
    let url = `api/messages`;

    options = options || {};
    let qs = {
      server: server,
      page: options.page,
      itemsPerPage: options.itemsPerPage
    };

    if (!optionalCallback) {
      return new Promise((resolve, reject) => {
        self.client.request.get(url, { qs: qs }, (err, response, body) => {
          if (err || response.statusCode !== 200) {
            return reject(new MailosaurError(response));
          }
          resolve(new MessageListResult(body));
        });
      });
    } else {
      self.client.request.get(url, { qs: qs }, (err, response, body) => {
        if (err || response.statusCode !== 200) {
          return optionalCallback(new MailosaurError(response));
        }
        optionalCallback(null, new MessageListResult(body));
      });
    }
  }

  /**
   * @summary Delete all messages
   *
   * Permanently deletes all messages held by the specified server. This
   * operation cannot be undone. Also deletes any attachments related to each
   * message.
   *
   * @param {string} server The identifier of the server to be emptied.
   *
   * @param {function} [optionalCallback] - The optional callback.
   *
   * @returns {function|Promise} If a callback was passed as the last parameter
   * then it returns the callback else returns a Promise.
   *
   * {Promise} A promise is returned
   *
   *                      @resolve {null} - The deserialized result object.
   *
   *                      @reject {Error} - The error object.
   *
   * {function} optionalCallback(err, result, request, response)
   *
   *                      {Error}  err        - The Error object if an error occurred, null otherwise.
   *
   *                      {null} [result]   - The deserialized result object if an error did not occur.
   */
  deleteAll(server, optionalCallback) {
    let client = this.client;
    let self = this;
    let url = `api/messages`;

    let qs = {
      server: server
    };

    if (!optionalCallback) {
      return new Promise((resolve, reject) => {
        self.client.request.del(url, { qs: qs }, (err, response, body) => {
          if (err || response.statusCode !== 204) {
            return reject(new MailosaurError(response));
          }
          resolve();
        });
      });
    } else {
      self.client.request.del(url, { qs: qs }, (err, response, body) => {
        if (err || response.statusCode !== 204) {
          return optionalCallback(new MailosaurError(response));
        }
        optionalCallback(null);
      });
    }
  }

  /**
   * @summary Search for messages
   *
   * Returns a list of messages matching the specified search criteria, in
   * summary form. The messages are returned sorted by received date, with the
   * most recently-received messages appearing first.
   *
   * @param {string} server The identifier of the server hosting the messages.
   *
   * @param {object} criteria The search criteria to match results against.
   *
   * @param {string} [criteria.sentTo] The full email address to which the target
   * email was sent.
   *
   * @param {string} [criteria.subject] The value to seek within the target
   * email's subject line.
   *
   * @param {string} [criteria.body] The value to seek within the target email's
   * HTML or text body.
   *
   * @param {object} [options] Optional Parameters.
   *
   * @param {number} [options.page] Used in conjunction with `itemsPerPage` to
   * support pagination.
   *
   * @param {number} [options.itemsPerPage] A limit on the number of results to
   * be returned per page. Can be set between 1 and 1000 items, the default is
   * 50.
   *
   * @param {number} [options.timeout] Specify how long to wait for a matching
   * result (in milliseconds).
   *
   * @param {date} [options.receivedAfter] Limits results to only messages
   * received after this date/time.
   *
   * @param {function} [optionalCallback] - The optional callback.
   *
   * @returns {function|Promise} If a callback was passed as the last parameter
   * then it returns the callback else returns a Promise.
   *
   * {Promise} A promise is returned
   *
   *                      @resolve {MessageListResult} - The deserialized result object.
   *
   *                      @reject {Error} - The error object.
   *
   * {function} optionalCallback(err, result, request, response)
   *
   *                      {Error}  err        - The Error object if an error occurred, null otherwise.
   *
   *                      {object} [result]   - The deserialized result object if an error did not occur.
   *                      See {@link MessageListResult} for more information.
   */
  search(server, criteria, options, optionalCallback) {
    let client = this.client;
    let self = this;
    let url = `api/messages/search`;
    let pollCount = 0;
    let startTime = Date.now();

    options = options || {};
    let qs = {
      server: server,
      page: options.page,
      itemsPerPage: options.itemsPerPage,
      receivedAfter: options.receivedAfter
    };

    if (!Number.isInteger(options.timeout)) {
      options.timeout = 0;
    }

    if (!optionalCallback) {
      let fn = (resolve, reject) => function() {
        self.client.request.post(url, { qs: qs, body: criteria }, (err, response, body) => {
          if (err || response.statusCode !== 200) {
              return reject(new MailosaurError(response));
          }

          if (options.timeout && !body.items.length) {
            let delayPattern = (response.headers['x-ms-delay'] || '1000')
              .split(',')
              .map(x => parseInt(x, 10));

            let delay = (pollCount >= delayPattern.length) ?
              delayPattern[delayPattern.length - 1] :
              delayPattern[pollCount];

            pollCount++;

            // Stop if timeout will be exceeded
            if (((Date.now() - startTime) + delay) > options.timeout) {
              return reject(new MailosaurError(response));
            }

            return setTimeout(fn(resolve, reject), delay);
          }

          resolve(new MessageListResult(body));
        });
      };

      return new Promise((resolve, reject) => {
        fn(resolve, reject)();
      });
    } else {
      let fn = () => {
        self.client.request.post(url, { qs: qs, body: criteria }, (err, response, body) => {
          if (err || response.statusCode !== 200) {
            return optionalCallback(new MailosaurError(response));
          }

          if (options.timeout && !body.items.length) {
            let delayPattern = (response.headers['x-ms-delay'] || '1000')
              .split(',')
              .map(x => parseInt(x, 10));

            let delay = (pollCount >= delayPattern.length) ?
              delayPattern[delayPattern.length - 1] :
              delayPattern[pollCount];

            pollCount++;

            // Stop if timeout will be exceeded
            if (((Date.now() - startTime) + delay) > options.timeout) {
              return optionalCallback(new MailosaurError(response));
            }

            return setTimeout(fn(resolve, reject), delay);
          }

          optionalCallback(null, new MessageListResult(body));
        });
      };

      fn();
    }
  }
}

module.exports = Messages;
<|endoftext|>


<commit_before>$(function(){
  $('.bioinfo-expand').click(function(e){
  // this = a[href=#$(tr).attr('id')];
    e.preventDefault();
    tr = $(this).parent().parent();

    if($(this).hasClass('expanded')){
      collapse(tr);
    } else {
      $(this).addClass('expanded');
      tr_id = $(tr).attr('id');
      $('tr[data-parent=#'+tr_id+']').show();
      $(this).find('span').toggleClass('glyphicon-chevron-right, glyphicon-chevron-down');
    }
  });
});

function collapse(element) {
  var element_id = $(element).attr('id');
  var expanded = $(element).find("a[href=#"+element_id+"]");
  $(expanded).removeClass('expanded');
  $(element).find('span.glyphicon').toggleClass('glyphicon-chevron-right, glyphicon-chevron-down');
  var children = $(element).parent().find('tr[data-parent=#'+element_id+']')
  $.each(children, function(index, child) {
    $(child).hide();
    collapse(child);
  });
};

$('.bioinfo-running-notes-save').click(function(e) {
    var td = $(this).parent();
    var tr = $(td).parent();
    var running_note = $(td).find('div[contenteditable="true"]').text();

    // do not save an empty value or if the running note wasn't modified
    if (running_note == "" || $(td).attr('data-running-note') == running_note) {
        return false;
    }

    var tr_class = $(tr).attr('class');
    var tr_id = $(tr).attr('id');
    tr_id = tr_id.replace('bioinfo-', '').split('-');
    var project_id = tr_id[0];
    var run_id = tr_id[1];
    var lane_id = tr_id[2];
    var sample_id = tr_id[3];

    // join not undefined values -> if undefined, will skip
    var prefix = [run_id, lane_id, sample_id].join(' ');

    running_note = prefix + '::: ' + running_note;

    var url='/api/v1/running_notes/' + project_id;
    var button = $(this);
    $(button).addClass('disabled').text('Submitting..');

    $.ajax({
        type: 'POST',
        url: url,
        dataType: 'json',
        data: {"note": running_note},
        error: function(xhr, textStatus, errorThrown) {
            alert('Error: '+xhr['responseText']+' ('+errorThrown+')');
            $(button).removeClass('disabled').text('Save');
            console.log(xhr);
            console.log(textStatus);
            console.log(errorThrown);
        },
        success: function(data, textStatus, xhr) {
            $(button).removeClass('disabled').text('Save');
            $(td).attr('data-running-note', data['note']);
            $('#bioinfo-delivery-project-'+project_id).find('div.bi-project-note').text(data['note']);
            // todo: add to the project running_notes
            var new_note_html = '
 \

'+ data['user'] + ' - '+ data["timestamp"]+'
 \

'+data['note']+'

';
            console.log(new_note_html);
            $('#running_notes_panels').prepend(new_note_html);
        }
    });
});

$('.bioinfo-running-notes-cancel').click(function(e) {
    var td = $(this).parent();
    var tr = $(td).parent();
    var running_note = $(td).attr('data-running-note');
    $(td).find('p').text(running_note);
});

$('.deliveries-page').on('click', '.runningNotesModal_button', function(e){
    var button = $(this);
    var project_id = $(button).parents('div.delivery').attr('id');
    project_id = project_id.replace('bioinfo-delivery-project-', '');
    // hopefully this will replace the project value in the running_notes.js
    project = project_id;
    var project_notes = $('.running-notes-panel[id^=running-note-'+project_id+']');
    $.each(project_notes, function(i, running_note){
        $(running_note).show();
    });
    $('#runningNotesModal_title').text('Running Notes for project '+project_id);
});

$('#runningNotesModal').on('hidden.bs.modal', function (e) {
  var running_notes = $('.running-notes-panel');
    $.each(running_notes, function(i, running_note){
        $(running_note).hide();
    });
})<commit_msg>Fixing markdown in the list of running_notes<commit_after>$(function(){
  $('.bioinfo-expand').click(function(e){
  // this = a[href=#$(tr).attr('id')];
    e.preventDefault();
    tr = $(this).parent().parent();

    if($(this).hasClass('expanded')){
      collapse(tr);
    } else {
      $(this).addClass('expanded');
      tr_id = $(tr).attr('id');
      $('tr[data-parent=#'+tr_id+']').show();
      $(this).find('span').toggleClass('glyphicon-chevron-right, glyphicon-chevron-down');
    }
  });
});

function collapse(element) {
  var element_id = $(element).attr('id');
  var expanded = $(element).find("a[href=#"+element_id+"]");
  $(expanded).removeClass('expanded');
  $(element).find('span.glyphicon').toggleClass('glyphicon-chevron-right, glyphicon-chevron-down');
  var children = $(element).parent().find('tr[data-parent=#'+element_id+']')
  $.each(children, function(index, child) {
    $(child).hide();
    collapse(child);
  });
};

$('.bioinfo-running-notes-save').click(function(e) {
    var td = $(this).parent();
    var tr = $(td).parent();
    var running_note = $(td).find('div[contenteditable="true"]').text();

    // do not save an empty value or if the running note wasn't modified
    if (running_note == "" || $(td).attr('data-running-note') == running_note) {
        return false;
    }

    var tr_class = $(tr).attr('class');
    var tr_id = $(tr).attr('id');
    tr_id = tr_id.replace('bioinfo-', '').split('-');
    var project_id = tr_id[0];
    var run_id = tr_id[1];
    var lane_id = tr_id[2];
    var sample_id = tr_id[3];

    // join not undefined values -> if undefined, will skip
    var prefix = [run_id, lane_id, sample_id].join(' ');

    running_note = prefix + '::: ' + running_note;

    var url='/api/v1/running_notes/' + project_id;
    var button = $(this);
    $(button).addClass('disabled').text('Submitting..');

    $.ajax({
        type: 'POST',
        url: url,
        dataType: 'json',
        data: {"note": running_note},
        error: function(xhr, textStatus, errorThrown) {
            alert('Error: '+xhr['responseText']+' ('+errorThrown+')');
            $(button).removeClass('disabled').text('Save');
            console.log(xhr);
            console.log(textStatus);
            console.log(errorThrown);
        },
        success: function(data, textStatus, xhr) {
            $(button).removeClass('disabled').text('Save');
            $(td).attr('data-running-note', data['note']);
            $('#bioinfo-delivery-project-'+project_id).find('div.bi-project-note').text(data['note']);
            // add to the project running_notes
            var new_note_html = '
 \

'+ data['user'] + ' - '+ data["timestamp"]+'
 \

'+data['note']+'

';
            // make_markdown is defined in base.js
            var markdown = make_markdown(new_note_html);
            $('#running_notes_panels').prepend(markdown);
        }
    });
});

$('.bioinfo-running-notes-cancel').click(function(e) {
    var td = $(this).parent();
    var tr = $(td).parent();
    var running_note = $(td).attr('data-running-note');
    $(td).find('p').text(running_note);
});

$('.deliveries-page').on('click', '.runningNotesModal_button', function(e){
    var button = $(this);
    var project_id = $(button).parents('div.delivery').attr('id');
    project_id = project_id.replace('bioinfo-delivery-project-', '');
    // hopefully this will replace the project value in the running_notes.js
    project = project_id;
    var project_notes = $('.running-notes-panel[id^=running-note-'+project_id+']');
    $.each(project_notes, function(i, running_note){
        $(running_note).show();
    });
    $('#runningNotesModal_title').text('Running Notes for project '+project_id);
});

$('#runningNotesModal').on('hidden.bs.modal', function (e) {
  var running_notes = $('.running-notes-panel');
    $.each(running_notes, function(i, running_note){
        $(running_note).hide();
    });
})<|endoftext|>


<commit_before>/**
 * jQuery plugin for getting position of cursor in textarea
 * @license under dfyw (do the fuck you want)
 * @author leChantaux (@leChantaux)
 */

(function ($, window, undefined) {
	// Create the defaults once
	var elementFactory = function (element, value) {
		element.text(value.val);
	};

	var pluginName = 'sew',
		defaults = {
			token: '@',
			elementFactory: elementFactory,
			values: [],
			unique: false,
			repeat: true
		};

	function Plugin(element, options) {
		this.element = element;
		this.$element = $(element);
		this.$itemList = $(Plugin.MENU_TEMPLATE);

		this.options = $.extend({}, defaults, options);
		this.reset();

		this._defaults = defaults;
		this._name = pluginName;

		this.expression = new RegExp('(^|\\b|\\s)' + this.options.token + '([\\w.]*)$');
		this.cleanupHandle = null;

		this.init();
	}

	Plugin.MENU_TEMPLATE = "

";

	Plugin.ITEM_TEMPLATE = '

';

	Plugin.KEYS = [40, 38, 13, 27, 9];

	Plugin.prototype.init = function () {
		if(this.options.values.length < 1) return;

		this.$element
									.bind('keyup', $.proxy(this.onKeyUp, this))
									.bind('keydown', $.proxy(this.onKeyDown, this))
									.bind('focus', $.proxy(this.renderElements, this, this.options.values))
									.bind('blur', $.proxy(this.remove, this));
	};

	Plugin.prototype.reset = function () {
		if(this.options.unique) {
			this.options.values = Plugin.getUniqueElements(this.options.values);
		}

		this.index = 0;
		this.matched = false;
		this.dontFilter = false;
		this.lastFilter = undefined;
		this.filtered = this.options.values.slice(0);
	};

	Plugin.prototype.next = function () {
		this.index = (this.index + 1) % this.filtered.length;
		this.hightlightItem();
	};

	Plugin.prototype.prev = function () {
		this.index = (this.index + this.filtered.length - 1) % this.filtered.length;
		this.hightlightItem();
	};

	Plugin.prototype.select = function () {
		this.replace(this.filtered[this.index].val);
		this.$element.trigger('mention-selected',this.filtered[this.index]);
		this.hideList();
	};

	Plugin.prototype.remove = function () {
		this.$itemList.fadeOut('slow');

		this.cleanupHandle = window.setTimeout($.proxy(function () {
			this.$itemList.remove();
		}, this), 1000);
	};

	Plugin.prototype.replace = function (replacement) {
		var startpos = this.$element.getCursorPosition();

		var fullStuff = this.getText();
		var val = fullStuff.substring(0, startpos);
		val = val.replace(this.expression, '$1' + this.options.token + replacement);

		var posfix = fullStuff.substring(startpos, fullStuff.length);
		var separator = posfix.match(/^\s/) ? '' : ' ';

		var finalFight = val + separator + posfix;
		this.setText(finalFight);
		this.$element.setCursorPosition(val.length + 1);
	};

	Plugin.prototype.hightlightItem = function () {
		this.$itemList.find(".-sew-list-item").removeClass("selected");

		var container = this.$itemList.find(".-sew-list-item").parent();
		var element = this.filtered[this.index].element.addClass("selected");

		var scrollPosition = element.position().top;
		container.scrollTop(container.scrollTop() + scrollPosition);
	};

	Plugin.prototype.renderElements = function (values) {
		$("body").append(this.$itemList);

		var container = this.$itemList.find('ul').empty();
		values.forEach($.proxy(function (e, i) {
			var $item = $(Plugin.ITEM_TEMPLATE);

			this.options.elementFactory($item, e);

			e.element = $item.appendTo(container).bind('click', $.proxy(this.onItemClick, this, e)).bind('mouseover', $.proxy(this.onItemHover, this, i));
		}, this));

		this.index = 0;
		this.hightlightItem();
	};

	Plugin.prototype.displayList = function () {
		if(!this.filtered.length) return;

		this.$itemList.show();
		var element = this.$element;
		var offset = this.$element.offset();
		var pos = element.getCaretPosition();

		this.$itemList.css({
			left: offset.left + pos.left,
			top: offset.top + pos.top
		});
	};

	Plugin.prototype.hideList = function () {
		this.$itemList.hide();
		this.reset();
	};

	Plugin.prototype.filterList = function (val) {
		if(val == this.lastFilter) return;

		this.lastFilter = val;
		this.$itemList.find(".-sew-list-item").remove();
		var values = this.options.values;

		var vals = this.filtered = values.filter($.proxy(function (e) {
			var exp = new RegExp('\\W*' + this.options.token + e.val + '(\\W|$)');
			if(!this.options.repeat && this.getText().match(exp)) {
				return false;
			}

			return	val === "" ||
							e.val.toLowerCase().indexOf(val.toLowerCase()) >= 0 ||
							(e.meta || "").toLowerCase().indexOf(val.toLowerCase()) >= 0;
		}, this));

		if(vals.length) {
			this.renderElements(vals);
			this.$itemList.show();
		} else {
			this.hideList();
		}
	};

	Plugin.getUniqueElements = function (elements) {
		var target = [];

		elements.forEach(function (e) {
			var hasElement = target.map(function (j) { return j.val; }).indexOf(e.val) >= 0;
			if(hasElement) return;
			target.push(e);
		});

		return target;
	};

	Plugin.prototype.getText = function () {
		return(this.$element.val() || this.$element.text());
	};

	Plugin.prototype.setText = function (text) {
		if(this.$element.is('input,textarea')) {
			this.$element.val(text);
		} else {
			this.$element.html(text);
		}
	};

	Plugin.prototype.onKeyUp = function (e) {
		var startpos = this.$element.getCursorPosition();
		var val = this.getText().substring(0, startpos);
		var matches = val.match(this.expression);

		if(!matches && this.matched) {
			this.matched = false;
			this.dontFilter = false;
			this.hideList();
			return;
		}

		if(matches && !this.matched) {
			this.displayList();
			this.lastFilter = "\n";
			this.matched = true;
		}

		if(matches && !this.dontFilter) {
			this.filterList(matches[2]);
		}
	};

	Plugin.prototype.onKeyDown = function (e) {
		var listVisible = this.$itemList.is(":visible");
		if(!listVisible || (Plugin.KEYS.indexOf(e.keyCode) < 0)) return;

		switch(e.keyCode) {
			case 9:
			case 13:
				this.select();
				break;
			case 40:
				this.next();
				break;
			case 38:
				this.prev();
				break;
			case 27:
				this.$itemList.hide();
				this.dontFilter = true;
				break;
		}

		e.preventDefault();
	};

	Plugin.prototype.onItemClick = function (element, e) {
		if(this.cleanupHandle) window.clearTimeout(this.cleanupHandle);

		this.replace(element.val);
		this.$element.trigger('mention-selected',this.filtered[this.index]);
		this.hideList();
	};

	Plugin.prototype.onItemHover = function (index, e) {
		this.index = index;
		this.hightlightItem();
	};

	$.fn[pluginName] = function (options) {
		return this.each(function () {
			if(!$.data(this, 'plugin_' + pluginName)) {
				$.data(this, 'plugin_' + pluginName, new Plugin(this, options));
			}
		});
	};
}(jQuery, window));
<commit_msg>Autocomplete tagged user name in running notes textarea: fix scroll issue<commit_after>/**
 * jQuery plugin for getting position of cursor in textarea
 * @license under dfyw (do the fuck you want)
 * @author leChantaux (@leChantaux)
 */

(function ($, window, undefined) {
	// Create the defaults once
	var elementFactory = function (element, value) {
		element.text(value.val);
	};

	var pluginName = 'sew',
		defaults = {
			token: '@',
			elementFactory: elementFactory,
			values: [],
			unique: false,
			repeat: true
		};

	function Plugin(element, options) {
		this.element = element;
		this.$element = $(element);
		this.$itemList = $(Plugin.MENU_TEMPLATE);

		this.options = $.extend({}, defaults, options);
		this.reset();

		this._defaults = defaults;
		this._name = pluginName;

		this.expression = new RegExp('(^|\\b|\\s)' + this.options.token + '([\\w.]*)$');
		this.cleanupHandle = null;

		this.init();
	}

	Plugin.MENU_TEMPLATE = "

";

	Plugin.ITEM_TEMPLATE = '

';

	Plugin.KEYS = [40, 38, 13, 27, 9];

	Plugin.prototype.init = function () {
		if(this.options.values.length < 1) return;

		this.$element
									.bind('keyup', $.proxy(this.onKeyUp, this))
									.bind('keydown', $.proxy(this.onKeyDown, this))
									.bind('focus', $.proxy(this.renderElements, this, this.options.values))
									.bind('blur', $.proxy(this.remove, this));
	};

	Plugin.prototype.reset = function () {
		if(this.options.unique) {
			this.options.values = Plugin.getUniqueElements(this.options.values);
		}

		this.index = 0;
		this.matched = false;
		this.dontFilter = false;
		this.lastFilter = undefined;
		this.filtered = this.options.values.slice(0);
	};

	Plugin.prototype.next = function () {
		this.index = (this.index + 1) % this.filtered.length;
		this.hightlightItem(true);
	};

	Plugin.prototype.prev = function () {
		this.index = (this.index + this.filtered.length - 1) % this.filtered.length;
		this.hightlightItem(true);
	};

	Plugin.prototype.select = function () {
		this.replace(this.filtered[this.index].val);
		this.$element.trigger('mention-selected',this.filtered[this.index]);
		this.hideList();
	};

	Plugin.prototype.remove = function () {
		this.$itemList.fadeOut('slow');

		this.cleanupHandle = window.setTimeout($.proxy(function () {
			this.$itemList.remove();
		}, this), 1000);
	};

	Plugin.prototype.replace = function (replacement) {
		var startpos = this.$element.getCursorPosition();

		var fullStuff = this.getText();
		var val = fullStuff.substring(0, startpos);
		val = val.replace(this.expression, '$1' + this.options.token + replacement);

		var posfix = fullStuff.substring(startpos, fullStuff.length);
		var separator = posfix.match(/^\s/) ? '' : ' ';

		var finalFight = val + separator + posfix;
		this.setText(finalFight);
		this.$element.setCursorPosition(val.length + 1);
	};

	Plugin.prototype.hightlightItem = function (scroll) {
		this.$itemList.find(".-sew-list-item").removeClass("selected");

		var container = this.$itemList.find(".-sew-list-item").parent();
		var element = this.filtered[this.index].element.addClass("selected");

		if(scroll){
			container.scrollTop(container.scrollTop() + scrollPosition);
			var scrollPosition = element.position().top;
			container.scrollTop(container.scrollTop() + scrollPosition);
		}
	};

	Plugin.prototype.renderElements = function (values) {
		$("body").append(this.$itemList);

		var container = this.$itemList.find('ul').empty();
		values.forEach($.proxy(function (e, i) {
			var $item = $(Plugin.ITEM_TEMPLATE);

			this.options.elementFactory($item, e);

			e.element = $item.appendTo(container).bind('click', $.proxy(this.onItemClick, this, e)).bind('mouseover', $.proxy(this.onItemHover, this, i));
		}, this));

		this.index = 0;
		this.hightlightItem();
	};

	Plugin.prototype.displayList = function () {
		if(!this.filtered.length) return;

		this.$itemList.show();
		var element = this.$element;
		var offset = this.$element.offset();
		var pos = element.getCaretPosition();

		this.$itemList.css({
			left: offset.left + pos.left,
			top: offset.top + pos.top
		});
	};

	Plugin.prototype.hideList = function () {
		this.$itemList.hide();
		this.reset();
	};

	Plugin.prototype.filterList = function (val) {
		if(val == this.lastFilter) return;

		this.lastFilter = val;
		this.$itemList.find(".-sew-list-item").remove();
		var values = this.options.values;

		var vals = this.filtered = values.filter($.proxy(function (e) {
			var exp = new RegExp('\\W*' + this.options.token + e.val + '(\\W|$)');
			if(!this.options.repeat && this.getText().match(exp)) {
				return false;
			}

			return	val === "" ||
							e.val.toLowerCase().indexOf(val.toLowerCase()) >= 0 ||
							(e.meta || "").toLowerCase().indexOf(val.toLowerCase()) >= 0;
		}, this));

		if(vals.length) {
			this.renderElements(vals);
			this.$itemList.show();
		} else {
			this.hideList();
		}
	};

	Plugin.getUniqueElements = function (elements) {
		var target = [];

		elements.forEach(function (e) {
			var hasElement = target.map(function (j) { return j.val; }).indexOf(e.val) >= 0;
			if(hasElement) return;
			target.push(e);
		});

		return target;
	};

	Plugin.prototype.getText = function () {
		return(this.$element.val() || this.$element.text());
	};

	Plugin.prototype.setText = function (text) {
		if(this.$element.is('input,textarea')) {
			this.$element.val(text);
		} else {
			this.$element.html(text);
		}
	};

	Plugin.prototype.onKeyUp = function (e) {
		if(Plugin.KEYS.indexOf(e.keyCode) > -1) return;
		var startpos = this.$element.getCursorPosition();
		var val = this.getText().substring(0, startpos);
		var matches = val.match(this.expression);

		if(!matches && this.matched) {
			this.matched = false;
			this.dontFilter = false;
			this.hideList();
			return;
		}

		if(matches && !this.matched) {
			this.displayList();
			this.lastFilter = "\n";
			this.matched = true;
		}

		if(matches && !this.dontFilter) {
			this.filterList(matches[2]);
		}
	};

	Plugin.prototype.onKeyDown = function (e) {
		var listVisible = this.$itemList.is(":visible");
		if(!listVisible || (Plugin.KEYS.indexOf(e.keyCode) < 0)) return;

		switch(e.keyCode) {
			case 9:
			case 13:
				this.select();
				break;
			case 40:
				this.next();
				break;
			case 38:
				this.prev();
				break;
			case 27:
				this.$itemList.hide();
				this.dontFilter = true;
				break;
		}

		e.preventDefault();
	};

	Plugin.prototype.onItemClick = function (element, e) {
		if(this.cleanupHandle) window.clearTimeout(this.cleanupHandle);

		this.replace(element.val);
		this.$element.trigger('mention-selected',this.filtered[this.index]);
		this.hideList();
	};

	Plugin.prototype.onItemHover = function (index, e) {
		this.index = index;
		this.hightlightItem();
	};

	$.fn[pluginName] = function (options) {
		return this.each(function () {
			if(!$.data(this, 'plugin_' + pluginName)) {
				$.data(this, 'plugin_' + pluginName, new Plugin(this, options));
			}
		});
	};
}(jQuery, window));
<|endoftext|>