<commit_before>/* global Calendar, moment */
{
  'use strict';

  class AppointmentAvailabilityCalendar extends Calendar {
    constructor(el, config = {}) {
      const calendarConfig = $.extend(true, {
        defaultView: 'agendaWeek',
        weekends: false,
        columnFormat: 'ddd D/M',
        slotDuration: '00:30:00',
        eventBorderColor: '#000',
        events: el.data('available-slots-path'),
        defaultDate: moment(el.data('default-date')),
        header: {
          'right': 'agendaWeek agendaThreeDay agendaDay today jumpToDate prev,next'
        },
        views: {
          agendaThreeDay: {
            type: 'agenda',
            duration: { days: 3 }
          }
        },
        eventRender: (event, element) => {
          event.$div = $(element);
          element.html(`

${event.start.format('HH:mm')}

             ${event.guiders}
          `);
          element.css({
            'max-width': '46%',
            'padding': '.2em',
            'box-sizing': 'border-box',
            'cursor': 'pointer',
            'background': this.getEventColour(event)
          });
        },
        eventClick: (event) => {
          const events = this.$el.fullCalendar('clientEvents');

          let start = '',
                end = '';

          for (let eventIndex in events) {
            let currentEvent = events[eventIndex];

            if (currentEvent === event && event.selected === false) {
              start = currentEvent.start.format();
              end = currentEvent.end.format();
              currentEvent.selected = true;
            } else {
              currentEvent.selected = false;
            }
          }

          this.$selectedStart.val(start);
          this.$selectedEnd.val(end);

          this.$el.fullCalendar('rerenderEvents');
        }
      }, config);

      super(el, calendarConfig);

      this.coloursConfig = [
        { count: 1,  colour: 'hsl(200, 50%, 65%)' },
        { count: 5,  colour: 'hsl(200, 50%, 55%)' },
        { count: 10, colour: 'hsl(200, 50%, 45%)' },
        { count: 20, colour: 'hsl(200, 50%, 35%)' },
        { count: 30, colour: 'hsl(200, 50%, 25%)' }
      ];

      this.init();
      this.selectEvent();
      this.displayErrorBorder();
    }

    getEventColour(event) {
      if (event.selected === true) {
        return this.selectedEventColour;
      }

      const colours = $.grep(this.coloursConfig, (configItem) => {
        if (configItem.count <= event.guiders) {
          return configItem.colour;
        }
      });

      return colours.pop().colour;
    }

    init() {
      this.$selectedStart = $('[data-selected-start]');
      this.$selectedEnd = $('[data-selected-end]');
      this.selectedEventColour = 'green';
    }

    selectEvent() {
      const events = this.$el.fullCalendar('clientEvents'),
      start = this.$selectedStart.val(),
      end = this.$selectedEnd.val();

      if ((!start && end)) {
        return;
      }

      for (let eventIndex in events) {
        let event = events[eventIndex];

        if (moment(start).isSame(event.start) && moment(end).isSame(event.end)) {
          this.$el.fullCalendar('gotoDate', start);
          event.$div.trigger('click');
          return;
        }
      }
    }

    displayErrorBorder() {
      if ($('.js-slot-unavailable-message').length > 0) {
        this.$el.find('.fc-view-container').addClass('error');
      }
    }
  }

  window.PWTAP = window.PWTAP || {};
  window.PWTAP.AppointmentAvailabilityCalendar = AppointmentAvailabilityCalendar;
}
<commit_msg>Ensure that reselecting a slot works when errors on the form<commit_after>/* global Calendar, moment */
{
  'use strict';

  class AppointmentAvailabilityCalendar extends Calendar {
    constructor(el, config = {}) {
      const calendarConfig = $.extend(true, {
        defaultView: 'agendaWeek',
        weekends: false,
        columnFormat: 'ddd D/M',
        slotDuration: '00:30:00',
        eventBorderColor: '#000',
        events: el.data('available-slots-path'),
        defaultDate: moment(el.data('default-date')),
        header: {
          'right': 'agendaWeek agendaThreeDay agendaDay today jumpToDate prev,next'
        },
        views: {
          agendaThreeDay: {
            type: 'agenda',
            duration: { days: 3 }
          }
        },
        eventRender: (event, element) => {
          event.$div = $(element);
          element.html(`

${event.start.format('HH:mm')}

             ${event.guiders}
          `);
          element.css({
            'max-width': '46%',
            'padding': '.2em',
            'box-sizing': 'border-box',
            'cursor': 'pointer',
            'background': this.getEventColour(event)
          });
        },
        eventClick: (event) => {
          const events = this.$el.fullCalendar('clientEvents');

          let start = '',
                end = '';

          for (let eventIndex in events) {
            let currentEvent = events[eventIndex];

            if (currentEvent === event && event.selected === false) {
              start = currentEvent.start.format();
              end = currentEvent.end.format();
              currentEvent.selected = true;
            } else {
              currentEvent.selected = false;
            }
          }

          this.$selectedStart.val(start);
          this.$selectedEnd.val(end);

          this.$el.fullCalendar('rerenderEvents');
        },
        loading: (isLoading) => {
          if (!isLoading) {
            this.selectEvent();
          }
        }
      }, config);

      super(el, calendarConfig);

      this.coloursConfig = [
        { count: 1,  colour: 'hsl(200, 50%, 65%)' },
        { count: 5,  colour: 'hsl(200, 50%, 55%)' },
        { count: 10, colour: 'hsl(200, 50%, 45%)' },
        { count: 20, colour: 'hsl(200, 50%, 35%)' },
        { count: 30, colour: 'hsl(200, 50%, 25%)' }
      ];

      this.init();
      this.displayErrorBorder();
    }

    getEventColour(event) {
      if (event.selected === true) {
        return this.selectedEventColour;
      }

      const colours = $.grep(this.coloursConfig, (configItem) => {
        if (configItem.count <= event.guiders) {
          return configItem.colour;
        }
      });

      return colours.pop().colour;
    }

    init() {
      this.$selectedStart = $('[data-selected-start]');
      this.$selectedEnd = $('[data-selected-end]');
      this.selectedEventColour = 'green';
    }

    selectEvent() {
      const events = this.$el.fullCalendar('clientEvents'),
      start = this.$selectedStart.val(),
      end = this.$selectedEnd.val();

      if ((!start && end)) {
        return;
      }

      for (let eventIndex in events) {
        let event = events[eventIndex];

        if (moment(start).isSame(event.start) && moment(end).isSame(event.end)) {
          this.$el.fullCalendar('gotoDate', start);
          event.$div.trigger('click');
          return;
        }
      }
    }

    displayErrorBorder() {
      if ($('.js-slot-unavailable-message').length > 0) {
        this.$el.find('.fc-view-container').addClass('error');
      }
    }
  }

  window.PWTAP = window.PWTAP || {};
  window.PWTAP.AppointmentAvailabilityCalendar = AppointmentAvailabilityCalendar;
}
<|endoftext|>


<commit_before>const reactionToolbarSymbol = [
  {
    name: "water-free",
    ops: [
      { "insert": "The reaction has been conducted in dry glass ware under inert atmosphere." }
    ]
  },
  {
    name: "resin-solvent",
    ops: [
      { "insert": "The resin (xxx mg, loading = X.XX g/mol, XX.X mmol) was swollen in xx mL of SOLVENT for xx min at room temperature." }
    ]
  },
  {
    name: "resin-solvent-reagent",
    ops: [
      { "insert": "The resin (xxx mg, loading = X.XX g/mol, XX.X mmol) was filled into a 10 mL crimp cap vial and was swollen in xx mL of SOLVENT. After xx min, xxx.x mg of REAGENT (XX.X mmol, XX.X equiv.) and XX.X mg of REAGENT (XX.X mg, XX.X mmol, X.XX equiv.) were added. The reaction mixture was shaken at XX °C for XX h." }
    ]
  },
  {
    name: "hand-stop",
    ops: [
      { "insert": "After complete conversion of the starting material, the reaction was quenched " },
      {
        "insert": "via",
        "attributes": { "italic": true}
      },
      { "insert": " careful addition of saturated NaHCO" },
      {
        "insert": "3",
        "attributes": { "script": "sub"}
      },
      { "insert": "-solution." },
    ]
  },
  {
    name: "reaction-procedure",
    ops: [
      { "insert": "The reaction mixture was poured into a glass funnel with filter paper and the polymer beads were washed XX times according to the following procedure: (1) SOLVENT [x repetitions] (2)  SOLVENT [x repetitions] (3)  SOLVENT [x repetitions] (4)  SOLVENT [x repetitions] (5)  SOLVENT [x repetitions]." }
    ]
  },
  {
    name: "gpx-a",
    ops: [
      { "insert": "According to GPX, AMOUNT g (XXX mmol, XX equiv.) of STARTING MATERIAL were reacted with XX.X mL (XX.X mg, XX.X mmol, X.XX equiv.) of REAGENT und X.XX mg (24.0 mmol, 5.00 equiv.) of REAGENT in XX mL of SOLVENT at XX °C for XX h." }
    ]
  },
  {
    name: "gpx-b",
    ops: [
      { "insert": "According to GPX, AMOUNT g (XXX mmol, XX equiv.) of STARTING MATERIAL were reacted with XX.X mL (XX.X mg, XX.X mmol, X.XX equiv.) of REAGENT und X.XX mg (24.0 mmol, 5.00 equiv.) of REAGENT in XX mL of SOLVENT at XX °C for XX h." }
    ]
  },
  {
    name: "washed-nahco3",
    ops: [
      { "insert": "The reaction mixture was poured into a separation funnel and the organic layer was washed successively with xx mL of NaHCO"},
      {
        "attributes": {"script":"sub"},
        "insert": "3"
      },
      { "insert": "-solution, xx mL of brine and xx mL of water. The aqueous layers were recombined and reextracted with ethyl acetate.\nThe organic layers were collected and were dried by the addition of Na" },
      {
        "attributes": {"script":"sub"},
        "insert": "2"
      },
      { "insert": "SO" },
      {
        "attributes": {"script":"sub"},
        "insert": "4"
      },
      { "insert": "/MgSO" },
      {
        "attributes": {"script":"sub"},
        "insert": "4"
      },
      { "insert": ". The mixture was filtered through a glass funnel and the solvent was evaporated under reduced pressure." }
    ]
  },
  {
    name: "acidified-hcl",
    ops: [
      { "insert": "The reaction mixture was poured into a separation funnel and was acidified by the addition of 1 M HCl. The aqueous layer was collected and adjusted to pH 8-9 by addition of saturated NaHCO" },
      {
        "attributes": {"script":"sub"},
        "insert": "3"
      },
      { "insert": "-solution. xx mL of SOLVENT were added and the aqueous phase was extracted three times. After washing with brine and water, the organic layers were collected and were dried by the addition of Na" },
      {
        "attributes": {"script":"sub"},
        "insert": "2"
      },
      { "insert":"SO" },
      {
        "attributes": {"script":"sub"},
        "insert": "4"
      },
      { "insert":"/MgSO" },
      {
        "attributes": {"script":"sub"},
        "insert": "4"
      },
      { "insert": ". The mixture was filtered through a glass funnel and the solvent was evaporated under reduced pressure." }
    ]
  },
  {
    name: "tlc-control",
    ops: [
      { "insert": "The progress of the reaction was observed via TLC control (cyclohexane:ethyl acetate; xx:xx; R"},
      {
        "attributes": {
          "italic": true,
          "script" :"sub"
        },
        "insert": "f = "
      },
      { "insert":"0.XX)." }
    ]
  },
  {
    name: "dried",
    ops: [
      { "insert": "The combined organic layers were dried by the addition of Na" },
      {
        "attributes": { "script": "sub" },
        "insert": "2"
      },
      { "insert":"SO" },
      {
        "attributes": { "script":"sub" },
        "insert":"4"
      },
      { "insert":"/MgSO" },
      {
        "attributes": { "script":"sub" },
        "insert": "4"
      },
      { "insert":". The mixture was filtered through a glass funnel and the solvent was evaporated under reduced pressure." }
    ]
  },
  {
    name: "isolated",
    ops: [
      { "insert": "The target compound was isolated by filtering of the resulting mixture through a glass funnel and was washed AMOUNT times with SOLVENT." }
    ]
  },
  {
    name: "residue-purified",
    ops: [
      { "insert": "The crude residue was purified " },
      {
        "attributes": { "italic": true },
        "insert": "via"
      },
      { "insert": " column chromatography (cyclohexane:ethyl acetate; xx:xx → cyclohexane:ethyl acetate; xx:xx). The target compound was isolated as a colorless solid in xx% yield (xx mg, xx mmol). R" },
      {
        "attributes": {
          "italic": true,
          "script": "sub"
        },
        "insert":"f = "
      },
      { "insert":"0.XX (cyclohexane:ethyl acetate)." }
    ]
  },
  {
    name: "residue-adsorbed",
    ops: [
      { "insert": "The crude residue was adsorbed on a small amount of silica gel/Celite and was purified " },
      {
        "attributes": { "italic": true },
        "insert": "via"
      },
      { "insert": " column chromatography (cyclohexane:ethyl acetate; xx:xx → cyclohexane:ethyl acetate; xx:xx). The target compound was isolated as a colorless solid in xx% yield (xx mg, xx mmol). R" },
      {
        "attributes": {
          "italic": true,
          "script": "sub"
        },
        "insert":"f = "
      },
      { "insert":"0.XX (cyclohexane:ethyl acetate)." }
    ]
  },
  {
    name: "residue-dissolved",
    ops: [
      { "insert": "The crude residue was dissolved in a small amount of SOLVENT and was purified " },
      {
        "attributes": { "italic": true },
        "insert": "via"
      },
      { "insert":" column chromatography (cyclohexane:ethyl acetate; xx:xx → cyclohexane:ethyl acetate; xx:xx). The target compound was isolated as a colorless STATE in xx% yield (xx mg, xx mmol). R" },
      {
        "attributes": {
          "italic": true,
          "script": "sub"
        },
        "insert": "f = "
      },
      { "insert":"0.XX (cyclohexane:ethyl acetate)." }
    ]
  },
]

const sampleAnalysesContentSymbol = [
  {
    name: "h-nmr",
    ops: [
      { "attributes": { "script":"super" }, "insert": "1" },
      { "insert":"H NMR (400 MHz, CDCl" },
      { "attributes": { "script":"sub" }, "insert": "3" },
      { "insert": "[MeOD/DMSO-d" },
      { "attributes": { "script":"sub" }, "insert": "6" },
      { "insert":"], ppm) δ =" }
    ]
  },
  {
    name: "c-nmr",
    ops: [
      { "attributes": { "script": "super" }, "insert": "13" },
      { "insert":"C NMR (100 MHz, CDCl" },
      { "attributes": { "script": "sub" }, "insert": "3" },
      { "insert": "[MeOD/DMSO-d" },
      { "attributes": { "script": "sub" }, "insert": "6" },
      { "insert":"], ppm) δ =" }
    ]
  },
  {
    name: "ir",
    ops: [
      { "insert": "IR (ATR, ṽ) = cm" },
      { "attributes": { "script": "super" }, "insert": "-1" },
      { "insert": "." }
    ]
  },
  {
    name: "ei",
    ops: [
      { "insert": "EI (m/z, 70 eV, XX °C): XXX (XX)." }
    ]
  },
  {
    name: "hr",
    ops: [
      { "insert": "HRMS (): calc. , found ." }
    ]
  },
  {
    name: "uv",
    ops: [
      { "insert": "UV-VIS (CH" },
      { "attributes": { "script": "sub" }, "insert": "2" },
      { "insert": "Cl" },
      { "attributes": { "script": "sub" }, "insert": "2" },
      { "insert": "), λ" },
      { "attributes": { "script": "sub" }, "insert": "max" },
      { "insert": "(log ε) = ." }    ]
  },
  {
    name: "ea",
    ops: [
      { "insert":"EA (): Calcd C ;H ;N ;O . Found C ;H ;N ;O ." }
    ]
  }
]

module.exports = {reactionToolbarSymbol, sampleAnalysesContentSymbol};
<commit_msg>Code re-format<commit_after>const reactionToolbarSymbol = [
  {
    name: 'water-free',
    ops: [
      { insert: 'The reaction has been conducted in dry glass ware under inert atmosphere.' },
    ],
  },
  {
    name: 'resin-solvent',
    ops: [
      { insert: 'The resin (xxx mg, loading = X.XX g/mol, XX.X mmol) was swollen in xx mL of SOLVENT for xx min at room temperature.' },
    ],
  },
  {
    name: 'resin-solvent-reagent',
    ops: [
      { insert: 'The resin (xxx mg, loading = X.XX g/mol, XX.X mmol) was filled into a 10 mL crimp cap vial and was swollen in xx mL of SOLVENT. After xx min, xxx.x mg of REAGENT (XX.X mmol, XX.X equiv.) and XX.X mg of REAGENT (XX.X mg, XX.X mmol, X.XX equiv.) were added. The reaction mixture was shaken at XX °C for XX h.' },
    ],
  },
  {
    name: 'hand-stop',
    ops: [
      { insert: 'After complete conversion of the starting material, the reaction was quenched ' },
      {
        insert: 'via',
        attributes: { italic: true },
      },
      { insert: ' careful addition of saturated NaHCO' },
      {
        insert: '3',
        attributes: { script: 'sub' },
      },
      { insert: '-solution.' },
    ],
  },
  {
    name: 'reaction-procedure',
    ops: [
      { insert: 'The reaction mixture was poured into a glass funnel with filter paper and the polymer beads were washed XX times according to the following procedure: (1) SOLVENT [x repetitions] (2)  SOLVENT [x repetitions] (3)  SOLVENT [x repetitions] (4)  SOLVENT [x repetitions] (5)  SOLVENT [x repetitions].' },
    ],
  },
  {
    name: 'gpx-a',
    ops: [
      { insert: 'According to GPX, AMOUNT g (XXX mmol, XX equiv.) of STARTING MATERIAL were reacted with XX.X mL (XX.X mg, XX.X mmol, X.XX equiv.) of REAGENT und X.XX mg (24.0 mmol, 5.00 equiv.) of REAGENT in XX mL of SOLVENT at XX °C for XX h.' },
    ],
  },
  {
    name: 'gpx-b',
    ops: [
      { insert: 'According to GPX, AMOUNT g (XXX mmol, XX equiv.) of STARTING MATERIAL were reacted with XX.X mL (XX.X mg, XX.X mmol, X.XX equiv.) of REAGENT und X.XX mg (24.0 mmol, 5.00 equiv.) of REAGENT in XX mL of SOLVENT at XX °C for XX h.' },
    ],
  },
  {
    name: 'washed-nahco3',
    ops: [
      { insert: 'The reaction mixture was poured into a separation funnel and the organic layer was washed successively with xx mL of NaHCO' },
      {
        attributes: { script: 'sub' },
        insert: '3',
      },
      { insert: '-solution, xx mL of brine and xx mL of water. The aqueous layers were recombined and reextracted with ethyl acetate.\nThe organic layers were collected and were dried by the addition of Na' },
      {
        attributes: { script: 'sub' },
        insert: '2',
      },
      { insert: 'SO' },
      {
        attributes: { script: 'sub' },
        insert: '4',
      },
      { insert: '/MgSO' },
      {
        attributes: { script: 'sub' },
        insert: '4',
      },
      { insert: '. The mixture was filtered through a glass funnel and the solvent was evaporated under reduced pressure.' },
    ],
  },
  {
    name: 'acidified-hcl',
    ops: [
      { insert: 'The reaction mixture was poured into a separation funnel and was acidified by the addition of 1 M HCl. The aqueous layer was collected and adjusted to pH 8-9 by addition of saturated NaHCO' },
      {
        attributes: { script: 'sub' },
        insert: '3',
      },
      { insert: '-solution. xx mL of SOLVENT were added and the aqueous phase was extracted three times. After washing with brine and water, the organic layers were collected and were dried by the addition of Na' },
      {
        attributes: { script: 'sub' },
        insert: '2',
      },
      { insert: 'SO' },
      {
        attributes: { script: 'sub' },
        insert: '4',
      },
      { insert: '/MgSO' },
      {
        attributes: { script: 'sub' },
        insert: '4',
      },
      { insert: '. The mixture was filtered through a glass funnel and the solvent was evaporated under reduced pressure.' },
    ],
  },
  {
    name: 'tlc-control',
    ops: [
      { insert: 'The progress of the reaction was observed via TLC control (cyclohexane:ethyl acetate; xx:xx; R' },
      {
        attributes: {
          italic: true,
          script: 'sub',
        },
        insert: 'f = ',
      },
      { insert: '0.XX).' },
    ],
  },
  {
    name: 'dried',
    ops: [
      { insert: 'The combined organic layers were dried by the addition of Na' },
      {
        attributes: { script: 'sub' },
        insert: '2',
      },
      { insert: 'SO' },
      {
        attributes: { script: 'sub' },
        insert: '4',
      },
      { insert: '/MgSO' },
      {
        attributes: { script: 'sub' },
        insert: '4',
      },
      { insert: '. The mixture was filtered through a glass funnel and the solvent was evaporated under reduced pressure.' },
    ],
  },
  {
    name: 'isolated',
    ops: [
      { insert: 'The target compound was isolated by filtering of the resulting mixture through a glass funnel and was washed AMOUNT times with SOLVENT.' },
    ],
  },
  {
    name: 'residue-purified',
    ops: [
      { insert: 'The crude residue was purified ' },
      {
        attributes: { italic: true },
        insert: 'via',
      },
      { insert: ' column chromatography (cyclohexane:ethyl acetate; xx:xx → cyclohexane:ethyl acetate; xx:xx). The target compound was isolated as a colorless solid in xx% yield (xx mg, xx mmol). R' },
      {
        attributes: {
          italic: true,
          script: 'sub',
        },
        insert: 'f = ',
      },
      { insert: '0.XX (cyclohexane:ethyl acetate).' },
    ],
  },
  {
    name: 'residue-adsorbed',
    ops: [
      { insert: 'The crude residue was adsorbed on a small amount of silica gel/Celite and was purified ' },
      {
        attributes: { italic: true },
        insert: 'via',
      },
      { insert: ' column chromatography (cyclohexane:ethyl acetate; xx:xx → cyclohexane:ethyl acetate; xx:xx). The target compound was isolated as a colorless solid in xx% yield (xx mg, xx mmol). R' },
      {
        attributes: {
          italic: true,
          script: 'sub',
        },
        insert: 'f = ',
      },
      { insert: '0.XX (cyclohexane:ethyl acetate).' },
    ],
  },
  {
    name: 'residue-dissolved',
    ops: [
      { insert: 'The crude residue was dissolved in a small amount of SOLVENT and was purified ' },
      {
        attributes: { italic: true },
        insert: 'via',
      },
      { insert: ' column chromatography (cyclohexane:ethyl acetate; xx:xx → cyclohexane:ethyl acetate; xx:xx). The target compound was isolated as a colorless STATE in xx% yield (xx mg, xx mmol). R' },
      {
        attributes: {
          italic: true,
          script: 'sub',
        },
        insert: 'f = ',
      },
      { insert: '0.XX (cyclohexane:ethyl acetate).' },
    ],
  },
];

const sampleAnalysesContentSymbol = [
  {
    name: 'h-nmr',
    ops: [
      { attributes: { script: 'super' }, insert: '1' },
      { insert: 'H NMR (400 MHz, CDCl' },
      { attributes: { script: 'sub' }, insert: '3' },
      { insert: '[MeOD/DMSO-d' },
      { attributes: { script: 'sub' }, insert: '6' },
      { insert: '], ppm) δ =' },
    ],
  },
  {
    name: 'c-nmr',
    ops: [
      { attributes: { script: 'super' }, insert: '13' },
      { insert: 'C NMR (100 MHz, CDCl' },
      { attributes: { script: 'sub' }, insert: '3' },
      { insert: '[MeOD/DMSO-d' },
      { attributes: { script: 'sub' }, insert: '6' },
      { insert: '], ppm) δ =' },
    ],
  },
  {
    name: 'ir',
    ops: [
      { insert: 'IR (ATR, ṽ) = cm' },
      { attributes: { script: 'super' }, insert: '-1' },
      { insert: '.' },
    ],
  },
  {
    name: 'ei',
    ops: [
      { insert: 'EI (m/z, 70 eV, XX °C): XXX (XX).' },
    ],
  },
  {
    name: 'hr',
    ops: [
      { insert: 'HRMS (): calc. , found .' },
    ],
  },
  {
    name: 'uv',
    ops: [
      { insert: 'UV-VIS (CH' },
      { attributes: { script: 'sub' }, insert: '2' },
      { insert: 'Cl' },
      { attributes: { script: 'sub' }, insert: '2' },
      { insert: '), λ' },
      { attributes: { script: 'sub' }, insert: 'max' },
      { insert: '(log ε) = .' },
    ],
  },
  {
    name: 'ea',
    ops: [
      { insert: 'EA (): Calcd C ;H ;N ;O . Found C ;H ;N ;O .' },
    ],
  },
];

module.exports = { reactionToolbarSymbol, sampleAnalysesContentSymbol };
<|endoftext|>


<commit_before>import React from 'react'
import './AllCardsView.scss'

export const AllCardsView = () => (

AllCardsView

)

export default AllCardsView
<commit_msg>Displaying basic card stats<commit_after>import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { getCards } from 'store/allCards'
import _ from 'lodash'
import './AllCardsView.scss'

export class AllCardsView extends Component {
  static propTypes = {
    allCards: PropTypes.object,
    getCards: PropTypes.func
  }

  render () {
    return (

AllCardsView

        <button className="btn" onClick={this.props.getCards}>Get</button>
        {this.props.allCards.fetching ?
Loading...
 : null}
        {this.props.allCards.error ?
{this.props.allCards.error}
 : null}

Latest set: {_.get(this.props.allCards, 'latestSet.name', '...')}

Size: {this.props.allCards.cardsNumber}

    )
  }
}

const mapDispatchToProps = { getCards }

const mapStateToProps = ({ allCards }) => ({ allCards })

export default connect(mapStateToProps, mapDispatchToProps)(AllCardsView)
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';
import Form from 'shared/components/form/form';
import FormButton from 'shared/components/form/formButton/formButton';
import FormInput from 'shared/components/form/formInput/formInput';
import FormSelect from 'shared/components/form/formSelect/formSelect';
import * as ApiHelpers from 'shared/utils/apiHelper';
import Section from 'shared/components/section/section';
// import styles from './squadsNew.css';

class SquadsNew extends Component {
  state = {
    mentors: [],
    name: null,
    loggedIn: true
  };

  componentDidMount() {
    Promise.all([ApiHelpers.getMentors()]).then((values) => {
      this.setState({
        mentors: values[0]
      });
    }).catch(this.setFetchError);
  }

  onUpdateSelect = (name, event) => {
    this.setState({
      [name]: event.target.value
    });
  }

  onNameChange = (value) => {
    this.setState({
      name: value
    });
  }

  buildMentorOptions = () =>
    this.state.mentors.map(mentor => ({ value: mentor.id, label: mentor.email }))

  handleOnClick = () => {
    ApiHelpers.postSquads({
      name: this.state.name,
      leader_id: this.state.leaderId,
      description: this.state.description,
      minimum: this.state.minimum,
      maximum: this.state.maximum,
      skill_level: this.state.skillLevel,
      activities: this.state.activities,
      end_condition: this.state.endCondition
    }).then(() => {
      this.setState({ success: true });
    }).catch(() => {
      this.setState({ error: 'There was an error creating a squad.' });
    });
  }

  render() {
    const { error, loggedIn, success } = this.state;
    if (!loggedIn) {
      return <Redirect to="/login" />;
    }
    return (

        { error &&
{error}
 }
        <Form>

Name

          <FormInput id="slackOrEmailSquadsNew" placeholder="Email or Slack Username" onChange={this.onNameChange} />

Squad Leader

Who will be leading this squad?

          <FormSelect
            id="squadLeader"
            prompt="Choose Squad Leader"
            options={this.buildMentorOptions()}
            onChange={e => this.onUpdateSelect('mentor', e)}
          />

Mentors

Which mentors will be assisting this squad?

          <FormSelect
            id="mentorsAssistingSquad"
            prompt="Choose Mentors to Assist Squad"
            options={this.buildMentorOptions()}
            onChange={e => this.onUpdateSelect('mentor', e)}
          />
          <FormButton text="Create Squad" onClick={this.handleOnClick} theme="red" />
          {success && <Redirect to="/thanks" />}
        </Form>

    );
  }
}
export default SquadsNew;
<commit_msg>Created the new squad form<commit_after>import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';
import Form from 'shared/components/form/form';
import FormButton from 'shared/components/form/formButton/formButton';
import FormInput from 'shared/components/form/formInput/formInput';
import FormSelect from 'shared/components/form/formSelect/formSelect';
// import FormButton from 'shared/components/form/formButton/formButton';
import * as ApiHelpers from 'shared/utils/apiHelper';
import Section from 'shared/components/section/section';
// import styles from './squadsNew.css';

class SquadsNew extends Component {
  state = {
    mentors: [],
    name: null,
    loggedIn: true
  };

  componentDidMount() {
    Promise.all([ApiHelpers.getMentors()]).then((values) => {
      this.setState({
        mentors: values[0]
      });
    }).catch(this.setFetchError);
  }

  onUpdateSelect = (name, event) => {
    this.setState({
      [name]: event.target.value
    });
  }

  onNameChange = (value) => {
    this.setState({
      name: value
    });
  }

  buildMentorOptions = () =>
    this.state.mentors.map(mentor => ({ value: mentor.id, label: mentor.email }))

  handleOnClick = () => {
    ApiHelpers.postSquads({
      name: this.state.name,
      leader_id: this.state.leaderId,
      description: this.state.description,
      minimum: this.state.minimum,
      maximum: this.state.maximum,
      skill_level: this.state.skillLevel,
      activities: this.state.activities,
      end_condition: this.state.endCondition
    }).then(() => {
      this.setState({ success: true });
    }).catch(() => {
      this.setState({ error: 'There was an error creating a squad.' });
    });
  }

  render() {
    const { error, loggedIn, success } = this.state;
    if (!loggedIn) {
      return <Redirect to="/login" />;
    }
    return (

        { error &&
{error}
 }
        <Form>

Name*

          <FormInput id="slackOrEmailSquadsNew" placeholder="Email or Slack Username" onChange={this.onNameChange} />

Squad name*

          <FormInput id="SquadsName" placeholder=" " onChange={this.onNameChange} />

Description*

          What will your squad be doing? Use this to get people excited to join!
          <FormInput id="Description" placeholder=" " onChange={this.onNameChange} />

Squad Leader

Who will be leading this squad?

          <FormSelect
            id="squadLeader"
            prompt="Choose Squad Leader"
            options={this.buildMentorOptions()}
            onChange={this.onNameChange}
          />

Mentors

Which mentors will be assisting this squad?

          <FormSelect
            id="mentorsAssistingSquad"
            prompt="Choose Mentors to Assist Squad"
            options={this.buildMentorOptions()}
            onChange={this.onNameChange}
          />

Minimum Numbers*

          How many members must join before you start?
          <FormInput id="MinNumbers" placeholder=" " onChange={this.onNameChange} />

Maximum Numbers*

          How many members are you willing to have total?
          <FormInput id="MaxNumbers" placeholder=" " onChange={this.onNameChange} />

Skill Level*

          What is the minimum skill level needed to participate?
          <select>
            <option value="beginner">Beginner</option>
            <option value="intermediate">Intermediate</option>
            <option value="advanced">Advanced</option>
          </select>

Weekly Activities*

          What does this squad schedule look like?
          <FormInput id="WeelyActivities" placeholder=" " onChange={this.onNameChange} />

End Condition*

          When will this squad end? This can be a date, a time period (4 weeks), or when a goal is met.
          <FormInput id="EndCondition" placeholder=" " onChange={this.onNameChange} />
          <FormButton text="Create Squad" onClick={this.handleOnClick} theme="red" />
          {success && <Redirect to="/thanks" />}
        </Form>

    );
  }
}
export default SquadsNew;
<|endoftext|>


<commit_before>/**
 * Created by Carson on 24/02/2017.
 * Implementation for searching. Users enter their search terms in a search bar, and the key parts
 * are used to look through the database of posts/comments. Implements AutoTag algorithm from Algorithmia to
 * automatically get the key parts of the search terms. Note that numbers (e.g. course numbers) are not considered
 * to be words, and will need to be parsed out manually and searched for in the tags column.
 * Those words are then checked to see how relevant they are to posts/comments in the database. This is done by Natural.
 *
 * need: npm install natural, npm install algorithmia
 *
 * AutoTag algorithm: https://algorithmia.com/algorithms/nlp/AutoTag
 * Natural Github: https://github.com/NaturalNode/natural
 *
 * Note: it is possible a small cost would be needed to use the AutoTag algorithm. Algorithmia credits are used to cover
 * the cost of the algorithm, which is estimated to be 11K credits per 10,000 calls. This would amount to ~1.17 USD per
 * 10,000 calls. However, for free, 5K credits are given each month, so on a small scale it is still free.
 */

//TODO search tags - wildcards to improve?
//TODO need to search multiple tables at once
//TODO stop holding entire database in memory when searching - recursiveGet

var natural = require("natural");
var algorithmia = require("algorithmia");
var lit = require('./../Literals.js');
var log = require('./../log.js');
var dbr = require('./../DBRow.js');

var TfIdf = natural.TfIdf;
var wordRelater = new TfIdf();

/**
 * Retrieves all tags currently in the database
 * @returns {Promise} Promise as database query is asynchronous. Eventually returns an array of Strings, one
 * for each tag in the database.
 */
function getTagsInDB() {
    var tags = [];
    return new Promise(function (resolve, reject) {
        var row = new dbr.DBRow(lit.TAG_TABLE);
        row.query().then(function () {
            while (row.next()) {
                tags.push(row.getValue(lit.FIELD_NAME));
            }
            resolve(tags);
        }).catch(function (err) {
            log.log("getTagsInDB error: " + err);
            reject(err);
        });
    });
}

/**
 * Function to search for posts by tags. Goes through search term looking for tags in database and numbers, then
 * finds posts with those tags/tags with that number.
 * @param inputSearch The search term.
 * @returns {Promise} Promise as query to database is asynchronous. Eventually returns an array of post IDs that have
 * matching tags/numbers.
 */
function searchByTag(inputSearch) {
    return new Promise(function (resolve, reject) {
        if (!(typeof inputSearch == lit.STRING)) {
            reject("you inputted an invalid search!");
        }
        getTagsInDB().then(function (dbTags) {
            var words = inputSearch.toUpperCase().split(/[ ,!?.]+/); //all possibilities for things splitting up words
            //and tags are all in upper case, so searched tags need to be as well

            return words.filter(function (element) {//take out words from the input search that don't include a tag in them
                //if the word is a tag if it is the same as a dbTag or if it has a number
                //often people refer to courses just by the number (e.g. you won't pass 112!)
                return dbTags.includes(element) || element.includes("1") || element.includes("2") || element.includes("3")
                    || element.includes("4") || element.includes("5") || element.includes("6") || element.includes("7")
                    || element.includes("8") || element.includes("9") || element.includes("0");
            });
        }).then(function (tagsInSearch) {
            //find posts with tags either matching a tag or that has the same course number that was searched
            var row = new dbr.DBRow(lit.POST_TABLE);
            var postsWithTags = [];
            row.query().then(function () {
                while (row.next()) {
                    for (var index in tagsInSearch) {
                        if (row.getValue(lit.FIELD_TAGS).includes(tagsInSearch[index])) {
                            postsWithTags.push(row.getValue(lit.FIELD_ID));
                        }
                    }
                }
                resolve(postsWithTags);
            }).catch(function (err) {
                log.log("searchByTag - getting posts - error: " + err);
                reject(err);
            });
        }).catch(function (err) {
            log.log("searchByTag: " + err);
        });
    });
}

//TODO talk with michael about how to do this
//new column on item/contribution with this stuff
// add some to actual tags column ONLY if user did not put in their own tags
//content type will be added to item/contribution
function tagPosts(newPostContent) {
    return new Promise(function (resolve, reject) {
            getKeyTerms(newPostContent).then(function (keyTerms) {
                var tags = "";
                for (var i in keyTerms) {
                    tags += keyTerms[i];
                    if (i < keyTerms.length - 1) {
                        tags += ", ";
                    }
                }
                resolve(tags); //string of all tags to be put into field for the new post
            }).catch(function (error) {
                log.log("tagPosts error: " + error);
                reject(error);
            });
        }
    );
}

//TODO use autotag column to speed up the search
/**
 * Searches a given array of table for data related to a given search. Fields are chosen for you, as the fields
 * that should be searched. Does not allow searches with bad search terms/tables to be conducted.
 * @param inputSearch Search inputted by user.
 * @param table Array of tables to be searched.
 */
function searchForContent(inputSearch, table) {
    return new Promise(function (resolve, reject) {
        if (goodInputs(inputSearch, table)) {
            getKeyTerms(inputSearch).then(function (keyTerms) {
                var fields = getSearchableFields(table);
                return searchTable(keyTerms, table, fields);
            }).then(function (documentInfo) {
                documentInfo = removeLowMeasures(documentInfo);
                resolve(sortByMeasure(documentInfo));
            }).catch(function (error) {
                log.log("searchForContent error: " + error);
            });
        } else {
            reject("your input terms didn't work for a search!");
        }
    });
}

/**
 * Sanitizes search input.
 * @param inputSearch The search term attempted.
 * @param table The table attempted to be searched.
 * @returns {boolean} True if all inputs are legitimate, else false.
 */
function goodInputs(inputSearch, table) {
    if (!(typeof inputSearch == lit.STRING) || inputSearch == undefined || inputSearch == "") {
        return false;
    } else {
        //check that table is actually a table name
        return !(!(typeof table == lit.STRING) || (table != lit.CLASS_TABLE && table != lit.POST_TABLE && table != lit.COMMENT_TABLE &&
        table != lit.LINK_TABLE && table != lit.TAG_TABLE && table != lit.USER_TABLE));
    }
}

/**
 * Returns an array of fields to be searched for each table. Hardcoded because there's not that many fields to
 * search and not that many tables that are searched.
 * @param table The table being searched.
 * @returns {Array} Array of the fields to be searched.
 */
function getSearchableFields(table) {
    var searchableFields = [];
    switch (table) {
        case lit.CLASS_TABLE:
            searchableFields.push(lit.FIELD_COURSE_CODE);
            searchableFields.push(lit.FIELD_TITLE);
            searchableFields.push(lit.FIELD_SUMMARY);
            searchableFields.push(lit.FIELD_LONG_SUMMARY);
            break;
        case lit.POST_TABLE:
            searchableFields.push(lit.FIELD_TITLE);
            searchableFields.push(lit.FIELD_CONTENT);
            break;
        case lit.COMMENT_TABLE:
            searchableFields.push(lit.FIELD_CONTENT);
            break;
        case lit.LINK_TABLE:
            searchableFields.push(lit.FIELD_TITLE);
            searchableFields.push(lit.FIELD_SUMMARY);
            break;
        case lit.TAG_TABLE:
            searchableFields.push(lit.FIELD_SUMMARY);
            searchableFields.push(lit.FIELD_RELATED_TAGS);
            searchableFields.push(lit.FIELD_NAME);
            break;
        case lit.USER_TABLE:
            searchableFields.push(lit.FIELD_USERNAME);
            break;
    }
    return searchableFields;
}

/**
 * Gets the key terms from an input String using Algoithmia Auto Tag API.
 * @param input String to get key terms from.
 * @returns {Promise} Promise as API is asynchronous. Eventually gets a String array of the key terms.
 */
function getKeyTerms(input) {
    return new Promise(function (resolve, reject) {
        algorithmia.client(lit.AUTO_TAG_API_KEY).algo(lit.AUTO_TAG_ALGORITHM).pipe(input)
            .then(function (response) {
                resolve(response.get());
            }, function (err) {
                reject(err);
            });
    });
}

/**
 * Function that takes data out of the database and finds its relation to the key terms of the search.
 * @param keyTerms Terms that data relation is found relative to.
 * @param table The table being searched.
 * @param fields The table fields being searched.
 * @returns {Promise} Promise as database query is asynchronous. Eventually returns an array of objects holding
 * content IDs and their relation to the key terms.
 */
function searchTable(keyTerms, table, fields) {
    var documentInfo = [];
    var row = new dbr.DBRow(table);
    return new Promise(function (resolve, reject) {
        //TODO use wildcards to filter rows returned by query
        row.query().then(function () {
            while (row.next()) {
                //search the post content and title
                var docData = "";
                for (var i in fields) {
                    docData += row.getValue(fields[i]) + "\n";
                }
                wordRelater.addDocument(docData);
                var docID = row.getValue(lit.FIELD_ID);
                var oneDoc = {measure: 0, id: docID};
                //add a row in the arrays for each document
                documentInfo.push(oneDoc);
            }
        }).then(function () {
            for (var termIndex in keyTerms) {
                wordRelater.tfidfs(keyTerms[termIndex], function (docIndex, measure) {
                    documentInfo[docIndex][lit.KEY_MEASURE] += measure;
                });
            }
            resolve(documentInfo);
        }).catch(function (error) {
            log.log("searchTable error: " + error);
            reject(error);
        });
    });
}

/**
 * Removes documents from search consideration that have too low of a measure.
 * @param documentInfo Documents being considered.
 * @returns {*} Array with low measures moved.
 */
function removeLowMeasures(documentInfo) {
    var i = 0;
    while (i < documentInfo.length) {
        if (documentInfo[i][lit.KEY_MEASURE] < lit.MIN_RELATION_MEASURE) { //remove the posts that aren't related enough
            documentInfo.splice(i, 1);
            //counter auto continued because the array decreased one size
        } else {
            i++;
        }
    }
    return documentInfo;
}

/**
 * Sorts document info by measure, and then returns an array of the document IDs in the same order.
 * @param documentInfo document array to sort by measure.
 * @returns {Array} Sorted document IDs.
 */
function sortByMeasure(documentInfo) {
    documentInfo = mergeSort(documentInfo);
    var sortedIDs = [];
    for (var index in documentInfo) {
        sortedIDs.push(documentInfo[index][lit.FIELD_ID]);
    }
    log.log(sortedIDs);
    return sortedIDs;
}

/**
 * Partitions the array being sorted by the Merge Sort algorithm.
 * @param arr Array to be sorted.
 * @returns {*} Sorted Array.
 */
function mergeSort(arr) {
    if (arr.length < 2)
        return arr;
    var middle = parseInt(arr.length / 2);
    var left = arr.slice(0, middle);
    var right = arr.slice(middle, arr.length);
    return merge(mergeSort(left), mergeSort(right));
}

/**
 * Merges partial arrays made by Merge Sort algorithm.
 * @param left Left array.
 * @param right Right array.
 * @returns {Array} Merged array. Eventually the fully sorted array.
 */
function merge(left, right) {
    var result = [];
    while (left.length && right.length) {
        if (left[0][lit.KEY_MEASURE] <= right[0][lit.KEY_MEASURE]) {
            result.push(left.shift());
        } else {
            result.push(right.shift());
        }
    }
    while (left.length)
        result.push(left.shift());
    while (right.length)
        result.push(right.shift());
    return result;
}<commit_msg>Fixed generateTags() and improved functions names<commit_after>/**
 * Created by Carson on 24/02/2017.
 * Implementation for searching. Users enter their search terms in a search bar, and the key parts
 * are used to look through the database of posts/comments. Implements AutoTag algorithm from Algorithmia to
 * automatically get the key parts of the search terms. Note that numbers (e.g. course numbers) are not considered
 * to be words, and will need to be parsed out manually and searched for in the tags column.
 * Those words are then checked to see how relevant they are to posts/comments in the database. This is done by Natural.
 *
 * need: npm install natural, npm install algorithmia
 *
 * AutoTag algorithm: https://algorithmia.com/algorithms/nlp/AutoTag
 * Natural Github: https://github.com/NaturalNode/natural
 *
 * Note: it is possible a small cost would be needed to use the AutoTag algorithm. Algorithmia credits are used to cover
 * the cost of the algorithm, which is estimated to be 11K credits per 10,000 calls. This would amount to ~1.17 USD per
 * 10,000 calls. However, for free, 5K credits are given each month, so on a small scale it is still free.
 */

//TODO need to search multiple tables at once
//TODO stop holding entire database in memory when searching - recursiveGet

var natural = require("natural");
var algorithmia = require("algorithmia");
var lit = require('./../Literals.js');
var log = require('./../log.js');
var dbr = require('./../DBRow.js');

var TfIdf = natural.TfIdf;
var wordRelater = new TfIdf();

/**
 * Retrieves all tags currently in the database
 * @returns {Promise} Promise as database query is asynchronous. Eventually returns an array of Strings, one
 * for each tag in the database.
 */
function getUserTagsInDB() {
    var tags = [];
    return new Promise(function (resolve, reject) {
        var row = new dbr.DBRow(lit.TAG_TABLE);
        row.query().then(function () {
            while (row.next()) {
                tags.push(row.getValue(lit.FIELD_NAME));
            }
            resolve(tags);
        }).catch(function (err) {
            log.log("getTagsInDB error: " + err);
            reject(err);
        });
    });
}

function searchByGenTag(searchTags, genTags) {
    return new Promise(function (resolve, reject) {
        if (typeof searchTags != lit.STRING || typeof genTags != lit.STRING) {
            reject("invalid tags");
        }
        //split generated tags into array
        var genTags = genTags.toUpperCase().split(" ");
    });
}

/**
 * Function to search for posts by tags. Goes through search term looking for tags in database and numbers, then
 * finds posts with those tags/tags with that number.
 * @param inputSearch The search term.
 * @returns {Promise} Promise as query to database is asynchronous. Eventually returns an array of post IDs that have
 * matching tags/numbers.
 */
function searchByUserTag(inputSearch) {
    return new Promise(function (resolve, reject) {
        if (!(typeof inputSearch == lit.STRING)) {
            reject("you inputted an invalid search!");
        }
        getUserTagsInDB().then(function (dbTags) {
            var words = inputSearch.toUpperCase().split(/[ ,!?.]+/); //all possibilities for things splitting up words
            //and tags are all in upper case, so searched tags need to be as well

            return words.filter(function (element) {//take out words from the input search that don't include a tag in them
                //if the word is a tag if it is the same as a dbTag or if it has a number
                //often people refer to courses just by the number (e.g. you won't pass 112!)
                return dbTags.includes(element) || element.includes("1") || element.includes("2") || element.includes("3")
                    || element.includes("4") || element.includes("5") || element.includes("6") || element.includes("7")
                    || element.includes("8") || element.includes("9") || element.includes("0");
            });
        }).then(function (tagsInSearch) {
            //find posts with tags either matching a tag or that has the same course number that was searched
            var row = new dbr.DBRow(lit.POST_TABLE);
            var postsWithTags = [];
            row.query().then(function () {
                while (row.next()) {
                    for (var index in tagsInSearch) {
                        if (row.getValue(lit.FIELD_TAGS).includes(tagsInSearch[index])) {
                            postsWithTags.push(row.getValue(lit.FIELD_ID));
                        }
                    }
                }
                resolve(postsWithTags);
            }).catch(function (err) {
                log.log("searchByTag - getting posts - error: " + err);
                reject(err);
            });
        }).catch(function (err) {
            log.log("searchByTag: " + err);
        });
    });
}

/**
 * Function that generates and inserts tags for content for later search usage.
 * @param newRow The row to generate tags for and to insert the tags into.
 * @returns {Promise} Promise as querying database is asynchronous. Eventually returns the generated tags,
 * which is one String with each tag concatenated by a space.
 */
function generateTags(newRow) {
    return new Promise(function (resolve, reject) {
            var content = newRow.getValue(lit.FIELD_TITLE) + "\n" + newRow.getValue(lit.FIELD_CONTENT);
            getKeyTerms(content).then(function (keyTerms) {
                var tags = "";
                for (var i in keyTerms) {
                    tags += keyTerms[i];
                    if (i < keyTerms.length - 1) {
                        tags += " "; //space splits up tags
                    }
                }
                newRow.setValue(lit.FIELD_GEN_TAGS, tags);
                newRow.update().then(function () {
                    resolve(tags); //string of all tags to be put into field for the new post
                });
            }).catch(function (error) {
                log.log("tagPosts error: " + error);
                reject(error);
            });
        }
    );
}

/**
 * Searches a given array of table for data related to a given search. Fields are chosen for you, as the fields
 * that should be searched. Does not allow searches with bad search terms/tables to be conducted.
 * @param inputSearch Search inputted by user.
 * @param table Array of tables to be searched.
 */
function searchForContent(inputSearch, table) {
    return new Promise(function (resolve, reject) {
        if (goodInputs(inputSearch, table)) {
            getKeyTerms(inputSearch).then(function (keyTerms) {
                var fields = getSearchableFields(table);
                return searchTable(keyTerms, table, fields);
            }).then(function (documentInfo) {
                documentInfo = removeLowMeasures(documentInfo);
                resolve(sortByMeasure(documentInfo));
            }).catch(function (error) {
                log.log("searchForContent error: " + error);
            });
        } else {
            reject("your input terms didn't work for a search!");
        }
    });
}

/**
 * Sanitizes search input.
 * @param inputSearch The search term attempted.
 * @param table The table attempted to be searched.
 * @returns {boolean} True if all inputs are legitimate, else false.
 */
function goodInputs(inputSearch, table) {
    if (!(typeof inputSearch == lit.STRING) || inputSearch == undefined || inputSearch == "") {
        return false;
    } else {
        //check that table is actually a table name
        return !(!(typeof table == lit.STRING) || (table != lit.CLASS_TABLE && table != lit.POST_TABLE && table != lit.COMMENT_TABLE &&
        table != lit.LINK_TABLE && table != lit.TAG_TABLE && table != lit.USER_TABLE));
    }
}

/**
 * Returns an array of fields to be searched for each table. Hardcoded because there's not that many fields to
 * search and not that many tables that are searched.
 * @param table The table being searched.
 * @returns {Array} Array of the fields to be searched.
 */
function getSearchableFields(table) {
    var searchableFields = [];
    switch (table) {
        case lit.CLASS_TABLE:
            searchableFields.push(lit.FIELD_COURSE_CODE);
            searchableFields.push(lit.FIELD_TITLE);
            searchableFields.push(lit.FIELD_SUMMARY);
            searchableFields.push(lit.FIELD_LONG_SUMMARY);
            break;
        case lit.POST_TABLE:
            searchableFields.push(lit.FIELD_TITLE);
            searchableFields.push(lit.FIELD_CONTENT);
            break;
        case lit.COMMENT_TABLE:
            searchableFields.push(lit.FIELD_CONTENT);
            break;
        case lit.LINK_TABLE:
            searchableFields.push(lit.FIELD_TITLE);
            searchableFields.push(lit.FIELD_SUMMARY);
            break;
        case lit.TAG_TABLE:
            searchableFields.push(lit.FIELD_SUMMARY);
            searchableFields.push(lit.FIELD_RELATED_TAGS);
            searchableFields.push(lit.FIELD_NAME);
            break;
        case lit.USER_TABLE:
            searchableFields.push(lit.FIELD_USERNAME);
            break;
    }
    return searchableFields;
}

/**
 * Gets the key terms from an input String using Algoithmia Auto Tag API.
 * @param input String to get key terms from.
 * @returns {Promise} Promise as API is asynchronous. Eventually gets a String array of the key terms.
 */
function getKeyTerms(input) {
    return new Promise(function (resolve, reject) {
        algorithmia.client(lit.AUTO_TAG_API_KEY).algo(lit.AUTO_TAG_ALGORITHM).pipe(input)
            .then(function (response) {
                resolve(response.get());
            }, function (err) {
                reject(err);
            });
    });
}

/**
 * Function that takes data out of the database and finds its relation to the key terms of the search.
 * @param keyTerms Terms that data relation is found relative to.
 * @param table The table being searched.
 * @param fields The table fields being searched.
 * @returns {Promise} Promise as database query is asynchronous. Eventually returns an array of objects holding
 * content IDs and their relation to the key terms.
 */
function searchTable(keyTerms, table, fields) {
    var documentInfo = [];
    var row = new dbr.DBRow(table);
    return new Promise(function (resolve, reject) {
        //TODO get generated tags,
        row.query().then(function () {
            while (row.next()) {
                //search the post content and title
                var docData = "";
                for (var i in fields) {
                    docData += row.getValue(fields[i]) + "\n";
                }
                wordRelater.addDocument(docData);
                var docID = row.getValue(lit.FIELD_ID);
                var oneDoc = {measure: 0, id: docID};
                //add a row in the arrays for each document
                documentInfo.push(oneDoc);
            }
        }).then(function () {
            for (var termIndex in keyTerms) {
                wordRelater.tfidfs(keyTerms[termIndex], function (docIndex, measure) {
                    documentInfo[docIndex][lit.KEY_MEASURE] += measure;
                });
            }
            resolve(documentInfo);
        }).catch(function (error) {
            log.log("searchTable error: " + error);
            reject(error);
        });
    });
}

/**
 * Removes documents from search consideration that have too low of a measure.
 * @param documentInfo Documents being considered.
 * @returns {*} Array with low measures moved.
 */
function removeLowMeasures(documentInfo) {
    var i = 0;
    while (i < documentInfo.length) {
        if (documentInfo[i][lit.KEY_MEASURE] < lit.MIN_RELATION_MEASURE) { //remove the posts that aren't related enough
            documentInfo.splice(i, 1);
            //counter auto continued because the array decreased one size
        } else {
            i++;
        }
    }
    return documentInfo;
}

/**
 * Sorts document info by measure, and then returns an array of the document IDs in the same order.
 * @param documentInfo document array to sort by measure.
 * @returns {Array} Sorted document IDs.
 */
function sortByMeasure(documentInfo) {
    documentInfo = mergeSort(documentInfo);
    var sortedIDs = [];
    for (var index in documentInfo) {
        sortedIDs.push(documentInfo[index][lit.FIELD_ID]);
    }
    log.log(sortedIDs);
    return sortedIDs;
}

/**
 * Partitions the array being sorted by the Merge Sort algorithm.
 * @param arr Array to be sorted.
 * @returns {*} Sorted Array.
 */
function mergeSort(arr) {
    if (arr.length < 2)
        return arr;
    var middle = parseInt(arr.length / 2);
    var left = arr.slice(0, middle);
    var right = arr.slice(middle, arr.length);
    return merge(mergeSort(left), mergeSort(right));
}

/**
 * Merges partial arrays made by Merge Sort algorithm.
 * @param left Left array.
 * @param right Right array.
 * @returns {Array} Merged array. Eventually the fully sorted array.
 */
function merge(left, right) {
    var result = [];
    while (left.length && right.length) {
        if (left[0][lit.KEY_MEASURE] <= right[0][lit.KEY_MEASURE]) {
            result.push(left.shift());
        } else {
            result.push(right.shift());
        }
    }
    while (left.length)
        result.push(left.shift());
    while (right.length)
        result.push(right.shift());
    return result;
}<|endoftext|>


<commit_before>import React, {Component} from 'react';
import {
    Text,
    View,
    TouchableOpacity,
    Picker,
    Platform,
    Switch
} from 'react-native';

import { SETTINGS_PANEL_STYLES } from 'app/appearance/styles/GlobalStyles';
import * as DateUtils from 'app/utility/transforms/DateUtils';
import SettingsEndSetTimerScreen from './timer/SettingsEndSetTimerScreen';
import SettingsMetric from './metric/SettingsMetric';

class SettingsApplicationPanel extends Component {

    // ACTIONS

    _tappedSetTimer() {
        this.props.tapEndSetTimer()
    }

    _tapDefaultMetric() {
        this.props.tapDefaultMetric()
    }

    _tappedSwitch(isSwitchOn) {
        if (isSwitchOn) {
            this.props.showRemoved();
        } else {
            this.props.hideRemoved();
        }
    }

    // RENDER

    _renderShowDeleted() {
        if (Platform.OS === 'ios') {
            return (
                <View>
                    <Text style={{fontSize: 16}}>Show deleted reps</Text>
                    <Switch
                        style={{backgroundColor: 'white', marginLeft: 3, marginRight: 5}}
                        value={this.props.shouldShowRemoved}
                        onValueChange={(isSwitchOn) => this._tappedSwitch(isSwitchOn)}
                        onTintColor='rgba(47, 128, 237, 1)'
                        thumbTintColor='lightgray'
                        tintColor='rgba(47, 128, 237, 1)'/>
                </View>
            );
        } else {
            return (
                <View style={{flex: 1, flexDirection: 'column'}}>
                    <Text style={{fontSize: 16, marginLeft: 7}}>Show deleted reps</Text>
                    <View style={{width: 55, marginTop: 10}}>
                        <Switch
                            value={this.props.shouldShowRemoved}
                            onValueChange={(isSwitchOn) => this._tappedSwitch(isSwitchOn)}
                            onTintColor='rgba(47, 128, 237, 1)'
                            thumbTintColor='lightgray'
                            tintColor='rgba(47, 128, 237, 1)'/>
                    </View>
                </View>
            );
        }
    }

    render() {
        if (Platform.OS === 'ios') {
            return (
                <View style={ [SETTINGS_PANEL_STYLES.panel, { flexDirection: 'column' }] }>
                    <Text style={{textAlign: 'center', fontSize: 20, marginBottom: 20}}>Settings</Text>
                    <View>
                        <TouchableOpacity onPress={() => this._tappedSetTimer()}>
                            <Text style={{fontSize: 16}}>
                                Time to start new set
                            </Text>
                            <Text style={{fontSize: 16, color: 'rgba(47, 128, 237, 1)'}}>
                                { DateUtils.timerDurationDescription(this.props.endSetTimerDuration) }{"\n"}
                            </Text>
                        </TouchableOpacity>
                    </View>
                    <SettingsEndSetTimerScreen />
                    <View style={{marginBottom: 15}}>
                        <TouchableOpacity onPress={() => this._tapDefaultMetric()}>
                            <Text style={{fontSize: 16}}>
                                Default units
                            </Text>
                            <Text style={{fontSize: 16, color: 'rgba(47, 128, 237, 1)'}}>
                                {this.props.defaultMetric}
                            </Text>
                        </TouchableOpacity>
                    </View>
                    <SettingsMetric />
                    {this._renderShowDeleted()}
                </View>
            );
        } else {
            return (
                <View style={ [SETTINGS_PANEL_STYLES.panel, { flexDirection: 'column' }] }>
                    <Text style={{textAlign: 'center', fontSize: 20, marginBottom: 13}}>Settings</Text>
                    <View>
                        <Text style={{fontSize: 16, marginLeft: 7}}>
                            Time to start new set
                        </Text>
                    </View>
                    <SettingsEndSetTimerScreen />
                    <View style={{marginTop: 10}}>
                        <Text style={{fontSize: 16, marginLeft: 7}}>
                            Set default metric
                        </Text>
                    </View>
                    <SettingsMetric />
                    {this._renderShowDeleted()}
                </View>
            );
        }
    }

}

export default SettingsApplicationPanel;
<commit_msg>Twaeked spacing in settings<commit_after>import React, {Component} from 'react';
import {
    Text,
    View,
    TouchableOpacity,
    Picker,
    Platform,
    Switch
} from 'react-native';

import { SETTINGS_PANEL_STYLES } from 'app/appearance/styles/GlobalStyles';
import * as DateUtils from 'app/utility/transforms/DateUtils';
import SettingsEndSetTimerScreen from './timer/SettingsEndSetTimerScreen';
import SettingsMetric from './metric/SettingsMetric';

class SettingsApplicationPanel extends Component {

    // ACTIONS

    _tappedSetTimer() {
        this.props.tapEndSetTimer()
    }

    _tapDefaultMetric() {
        this.props.tapDefaultMetric()
    }

    _tappedSwitch(isSwitchOn) {
        if (isSwitchOn) {
            this.props.showRemoved();
        } else {
            this.props.hideRemoved();
        }
    }

    // RENDER

    _renderShowDeleted() {
        if (Platform.OS === 'ios') {
            return (
                <View>
                    <Text style={{fontSize: 16, marginBottom: 2}}>Show deleted reps</Text>
                    <Switch
                        style={{backgroundColor: 'white', marginLeft: 3, marginRight: 5}}
                        value={this.props.shouldShowRemoved}
                        onValueChange={(isSwitchOn) => this._tappedSwitch(isSwitchOn)}
                        onTintColor='rgba(47, 128, 237, 1)'
                        thumbTintColor='lightgray'
                        tintColor='rgba(47, 128, 237, 1)'/>
                </View>
            );
        } else {
            return (
                <View style={{flex: 1, flexDirection: 'column', marginTop: 4}}>
                    <Text style={{fontSize: 16, marginLeft: 7}}>Show deleted reps</Text>
                    <View style={{width: 55, marginTop: 7}}>
                        <Switch
                            value={this.props.shouldShowRemoved}
                            onValueChange={(isSwitchOn) => this._tappedSwitch(isSwitchOn)}
                            onTintColor='rgba(47, 128, 237, 1)'
                            thumbTintColor='lightgray'
                            tintColor='rgba(47, 128, 237, 1)'/>
                    </View>
                </View>
            );
        }
    }

    render() {
        if (Platform.OS === 'ios') {
            return (
                <View style={ [SETTINGS_PANEL_STYLES.panel, { flexDirection: 'column' }] }>
                    <Text style={{textAlign: 'center', fontSize: 20, marginBottom: 20}}>Settings</Text>
                    <View>
                        <TouchableOpacity onPress={() => this._tappedSetTimer()}>
                            <Text style={{fontSize: 16, marginBottom: 2}}>
                                Time to start new set
                            </Text>
                            <Text style={{fontSize: 16, color: 'rgba(47, 128, 237, 1)'}}>
                                { DateUtils.timerDurationDescription(this.props.endSetTimerDuration) }{"\n"}
                            </Text>
                        </TouchableOpacity>
                    </View>
                    <SettingsEndSetTimerScreen />
                    <View style={{marginBottom: 15}}>
                        <TouchableOpacity onPress={() => this._tapDefaultMetric()}>
                            <Text style={{fontSize: 16, marginBottom: 2}}>
                                Default units
                            </Text>
                            <Text style={{fontSize: 16, color: 'rgba(47, 128, 237, 1)'}}>
                                {this.props.defaultMetric}
                            </Text>
                        </TouchableOpacity>
                    </View>
                    <SettingsMetric />
                    {this._renderShowDeleted()}
                </View>
            );
        } else {
            return (
                <View style={ [SETTINGS_PANEL_STYLES.panel, { flexDirection: 'column' }] }>
                    <Text style={{textAlign: 'center', fontSize: 20, marginBottom: 20}}>Settings</Text>
                    <View>
                        <Text style={{fontSize: 16, marginLeft: 7, marginBottom: -10}}>
                            Time to start new set
                        </Text>
                    </View>
                    <SettingsEndSetTimerScreen />
                    <View style={{marginTop: 10}}>
                        <Text style={{fontSize: 16, marginLeft: 7, marginBottom: -10}}>
                            Set default metric
                        </Text>
                    </View>
                    <SettingsMetric />
                    {this._renderShowDeleted()}
                </View>
            );
        }
    }

}

export default SettingsApplicationPanel;
<|endoftext|>


<commit_before>/* Tests for gpii-ipc.js
 *
 * Copyright 2017 Raising the Floor - International
 *
 * Licensed under the New BSD license. You may not use this file except in
 * compliance with this License.
 *
 * The R&D leading to these results received funding from the
 * Department of Education - Grant H421A150005 (GPII-APCP). However,
 * these results do not necessarily represent the policy of the
 * Department of Education, and you should not assume endorsement by the
 * Federal Government.
 *
 * You may obtain a copy of the License at
 * https://github.com/GPII/universal/blob/master/LICENSE.txt
 */

"use strict";

var jqUnit = require("node-jqunit"),
    net = require("net"),
    path = require("path"),
    fs = require("fs"),
    os = require("os"),
    child_process = require("child_process"),
    gpiiIPC = require("../src/gpii-ipc.js"),
    windows = require("../src/windows.js"),
    winapi = require("../src/winapi.js");

var teardowns = [];

jqUnit.module("GPII pipe tests", {
    teardown: function () {
        while (teardowns.length) {
            teardowns.pop()();
        }
    }
});

var createLogFile = function () {
    // The child process will write to this file (stdout isn't captured)
    var logFile = os.tmpdir() + "/gpii-test-output" + Date.now();
    console.log("logfile:", logFile);
    fs.writeFileSync(logFile, "");
    teardowns.push(function () {
        try {
            fs.unlinkSync(logFile);
        } catch (e) {
            // Ignored
        }
    });
    return logFile;
};

// Tests generatePipeName
jqUnit.test("Test generatePipeName", function () {
    var pipePrefix = "\\\\.\\pipe\\";

    // Because the names are random, check against a sample of them to avoid lucky results.
    var sampleSize = 300;
    var pipeNames = [];
    for (var n = 0; n < sampleSize; n++) {
        pipeNames.push(gpiiIPC.generatePipeName());
    }

    for (var pipeIndex = 0; pipeIndex < sampleSize; pipeIndex++) {
        var fullName = pipeNames[pipeIndex];

        // Pipe Names: https://msdn.microsoft.com/library/aa365783
        jqUnit.assertTrue("Pipe path must begin with " + pipePrefix, fullName.startsWith(pipePrefix));
        jqUnit.assertTrue("Entire pipe name must <= 256 characters", fullName.length <= 256);

        var pipeName = fullName.substr(pipePrefix.length);
        jqUnit.assertTrue("Pipe name must at least 1 character", pipeName.length > 0);
        // "any character other than a backslash, including numbers and special characters"
        // This also includes '/' because node swaps it with '\'.
        jqUnit.assertFalse("Pipe name must not contain a slash or blackslash", pipeName.match(/[\\\/]/));

        // There shouldn't be any repeated names in a sample size of this size.
        var dup = pipeNames.indexOf(fullName) !== pipeIndex;
        jqUnit.assertFalse("There shouldn't be any repeated pipe names", dup);
    }
});

jqUnit.asyncTest("Test createPipe", function () {
    jqUnit.expect(4);

    var pipeName = gpiiIPC.generatePipeName();

    var promise = gpiiIPC.createPipe(pipeName);
    jqUnit.assertNotNull("createPipe must return non-null", promise);
    jqUnit.assertEquals("createPipe must return a promise", "function", typeof(promise.then));

    promise.then(function (pipeServer) {
        jqUnit.assertTrue("createPipe should have resolved with a value", !!pipeServer);

        jqUnit.assertTrue("createPipe resolved value should be a net.Server instance",
            pipeServer instanceof net.Server);

        jqUnit.start();
    }, function (err) {
        console.error(err);
        jqUnit.fail("createPipe should have resolved");
    });
});

jqUnit.asyncTest("Test createPipe failures", function () {

    var existingPipe = gpiiIPC.generatePipeName();

    var pipeNames = [
        // A pipe that exists.
        existingPipe,
        // Badly formed name
        "invalid",
        null
    ];

    jqUnit.expect(pipeNames.length * 3);

    var testPipes = function (pipeNames) {
        var pipeName = pipeNames.shift();
        console.log("Checking bad pipe name:", pipeName);

        var promise = gpiiIPC.createPipe(pipeName);
        jqUnit.assertNotNull("createPipe must return non-null", promise);
        jqUnit.assertEquals("createPipe must return a promise", "function", typeof(promise.then));

        promise.then(function () {
            jqUnit.fail("createPipe should not have resolved");
        }, function () {
            jqUnit.assert("createPipe should reject");

            if (pipeNames.length > 0) {
                testPipes(pipeNames);
            } else {
                jqUnit.start();
            }
        });
    };

    // Create a pipe to see what happens if another pipe is created with the same name.
    gpiiIPC.createPipe(existingPipe).then(function () {
        // run the tests.
        testPipes(Array.from(pipeNames));
    }, function (err) {
        console.error(err);
        jqUnit.fail("initial createPipe failed");
    });

});

/**
 * Read from a pipe, calling callback with all the data when it ends.
 *
 * @param pipeName {String} Pipe name.
 * @param callback {Function(err,data)} What to call.
 */
function readPipe(pipeName, callback) {
    var buffer = "";
    var server = net.createServer(function (con) {
        con.setEncoding("utf8");
        buffer = "";

        con.on("error", function (err) {
            console.error(err);
            callback(err);
        });
        con.on("data", function (data) {
            buffer += data;
        });
        con.on("end", function () {
            callback(null, buffer);
        });
    });
    server.listen(pipeName);
    server.on("error", function (err) {
        console.error(err);
        jqUnit.fail("Error with the pipe server");
    });
}

// Tests the execution of a child process with gpiiIPC.execute
jqUnit.asyncTest("Test execute", function () {

    jqUnit.expect(4);

    var testData = {
        execOptions: {
            env: {
                "GPII_TEST_VALUE1": "value1",
                "GPII_TEST_VALUE2": "value2"
            },
            currentDir: process.env.APPDATA
        }
    };

    // Create a pipe so the child process can talk back (gpiiIPC.execute doesn't capture the child's stdout).
    var pipeName = gpiiIPC.generatePipeName();
    readPipe(pipeName, checkReturn);

    var options = Object.assign({}, testData.execOptions);

    // Two asserts for each environment value.
    jqUnit.expect(Object.keys(options.env).length * 2);

    // Start the child process, passing the pipe name.
    var script = path.join(__dirname, "gpii-ipc-tests-child.js");
    var command = ["node", script, "named-pipe", pipeName].join(" ");
    console.log("Executing", command);
    var pid = gpiiIPC.execute(command, options);

    jqUnit.assertEquals("execute should return a number", "number", typeof(pid));

    function checkReturn(err, data) {
        if (err) {
            console.error(err);
            jqUnit.fail("The was something wrong with the pipe");
        } else {
            var obj;

            try {
                obj = JSON.parse(data);
            } catch (e) {
                console.log("child returned: ", data);
                throw e;
            }

            jqUnit.assertTrue("child process should return something", !!obj);
            if (obj.error) {
                console.log("Error from child:", obj.error);
                jqUnit.fail("child process returned an error");
                return;
            }
            jqUnit.assertEquals("'currentDir' should return from child", options.currentDir, obj.currentDir);
            jqUnit.assertTrue("'env' should return from child", !!obj.env);

            for (var key in options.env) {
                var value = options.env[key];
                jqUnit.assertTrue("Environment should contain " + key, key, obj.hasOwnProperty(key));
                jqUnit.assertEquals("Environment '" + key + "' should be the expected value", value, obj.env[key]);
            }

            jqUnit.start();
        }
    }

    teardowns.push(function () {
        try {
            process.kill(pid);
        } catch (e) {
            // Ignored.
        }
    });

    // Set a timeout.
    windows.waitForProcessTermination(pid, 5000).then(function (value) {
        if (value === "timeout") {
            jqUnit.fail("Child process took too long.");
        }
    }, function (err) {
        console.error(err);
        jqUnit.fail("Unable to wait for child process.");
    });

});

// Tests validateClient
jqUnit.asyncTest("Test validateClient", function () {

    var tests = [
        {
            // SetEvent is called by this process.
            id: "successful",
            startChildProcess: false,
            setEvent: true,
            timeout: 20,
            expect: {
                end: false,
                resolve: true
            }
        },
        {
            // SetEvent is not called.
            id: "timeout",
            startChildProcess: false,
            setEvent: false,
            timeout: 2,
            expect: {
                end: true,
                resolve: false
            }
        },
        {
            // SetEvent is called by another process (that hasn't been granted the handle).
            id: "other-process",
            startChildProcess: true,
            setEvent: false,
            timeout: 2,
            expect: {
                end: true,
                resolve: false
            }
        }
    ];

    var runTest = function (testIndex) {
        if (testIndex >= tests.length) {
            jqUnit.start();
            return;
        }

        var test = tests[testIndex];
        var endCalled = true;

        // A mock pipe.
        var pipe = {
            write: function (data) {
                var eventHandleText = data.substr("event:".length).trim();
                jqUnit.assertFalse("event handle must be numeric", isNaN(eventHandleText));

                if (test.startChildProcess) {
                    var script = path.join(__dirname, "gpii-ipc-tests-child.js");
                    var command = ["node", script, "validate-client"].join(" ");
                    console.log("starting", command);
                    child_process.exec(command, function (err, stdout, stderr) {
                        console.log("child stdout:", stdout);
                        console.log("child stderr:", stderr);
                        if (err) {
                            jqUnit.fail(err);
                        } else {
                            jqUnit.assertTrue("child should have called SetEvent",
                                stdout.indexOf("SetEvent returned") > -1);
                        }
                    });

                }

                if (test.setEvent) {
                    var eventHandle = parseInt(eventHandleText);
                    winapi.kernel32.SetEvent(eventHandle);
                }
            },
            end: function () {
                endCalled = true;
            }
        };

        var pid = process.pid;
        gpiiIPC.validateClient(pipe, pid, test.timeout).then(function () {
            jqUnit.assertTrue("validateClient resolved", test.expect.resolve);
        }, function (e) {
            console.log(e);
            jqUnit.assertFalse("validateClient rejected", test.expect.resolve);
        }).then(function () {
            jqUnit.assertEquals("end should be called (if expected)", test.expect.end, endCalled);
            runTest(testIndex + 1);
        });
    };
    runTest(0);

});

// Tests startProcess - creates a child process using startProcess.
jqUnit.asyncTest("Test startProcess", function () {
    var logFile = createLogFile();
    var getLog = function () {
        return fs.readFileSync(logFile, {encoding: "utf8"});
    };

    var script = path.join(__dirname, "gpii-ipc-tests-child.js");
    var command = ["node", script, "inherited-pipe", logFile].join(" ");
    console.log("Starting", command);

    // 0: server sends challenge, client performs it, server sends "OK"
    // 1: child sends expected[0]
    // 2: parent sends sendData
    // 3: child sends expected[1]
    var sendData = "FROM PARENT" + Math.random();
    var expected = [
        // Child responds with what was sent (from the authentication).
        "received: OK\n",
        // Child responds with what was sent (after authentication).
        "received: " + sendData + "\n"
    ];

    var expectedIndex = 0;
    var complete = false;
    var allData = "";
    var pid = null;

    var promise = gpiiIPC.startProcess(command, "test-startProcess");

    jqUnit.assertNotNull("startProcess must return non-null", promise);
    jqUnit.assertEquals("startProcess must return a promise", "function", typeof(promise.then));

    promise.then(function (p) {
        jqUnit.assertNotNull("startProcess must return resolve with a value", p);
        jqUnit.assertFalse("startProcess resolved value pid field must be numeric", isNaN(p.pid));
        jqUnit.assertTrue("startProcess resolved value pipeServer field must be a net.Server instance",
            p.pipeServer instanceof net.Server);

        pid = p.pid;

        // Set a timeout.
        windows.waitForProcessTermination(pid, 5000).then(function (value) {
            var logContent = getLog();
            if (value === "timeout") {
                console.log(logContent);
                jqUnit.fail("Child process took too long.");
            } else {
                if (!complete) {
                    console.log(logContent);
                    jqUnit.fail("child should not terminate until completed");
                } else if (logContent.indexOf("FAIL") >= 0) {
                    console.log(logContent);
                    jqUnit.fail("Child process failed");
                }
            }
        }, function (err) {
            console.error(err);
            jqUnit.fail("Unable to wait for child process.");
        });

        p.pipeServer.on("error", function (err) {
            console.error("Pipe server error:", err);
            jqUnit.fail("Pipe server failed");
        });

        // Wait for the child to connect to the pipe
        p.pipeServer.on("connection", function (pipe) {
            console.log("connection from child");

            pipe.on("data", function (data) {
                allData += data;
                if (allData.indexOf("\n") >= 0) {
                    console.log("from pipe:", allData);
                    jqUnit.assertEquals("Expected input from pipe", expected[expectedIndex], allData);
                    allData = "";
                    expectedIndex++;
                    if (expectedIndex >= expected.length) {
                        complete = true;
                        pipe.end();
                    } else {
                        console.log("send: " + sendData);
                        pipe.write(sendData + "\n");
                    }
                }
            });

            pipe.on("end", function () {
                console.log("pipe end");
                if (complete) {
                    jqUnit.start();
                } else {
                    console.log(getLog());
                    jqUnit.fail("child should not terminate until completed");
                }
            });

            pipe.on("error", function (err) {
                console.error("Pipe error:", err);
                jqUnit.fail("Pipe failed.");
            });
        });

    }, jqUnit.fail);

    teardowns.push(function () {
        try {
            process.kill(pid);
        } catch (e) {
            // Ignored.
        }
    });
});
<commit_msg>GPII-2399: Fixed test.<commit_after>/* Tests for gpii-ipc.js
 *
 * Copyright 2017 Raising the Floor - International
 *
 * Licensed under the New BSD license. You may not use this file except in
 * compliance with this License.
 *
 * The R&D leading to these results received funding from the
 * Department of Education - Grant H421A150005 (GPII-APCP). However,
 * these results do not necessarily represent the policy of the
 * Department of Education, and you should not assume endorsement by the
 * Federal Government.
 *
 * You may obtain a copy of the License at
 * https://github.com/GPII/universal/blob/master/LICENSE.txt
 */

"use strict";

var jqUnit = require("node-jqunit"),
    net = require("net"),
    path = require("path"),
    fs = require("fs"),
    os = require("os"),
    child_process = require("child_process"),
    gpiiIPC = require("../src/gpii-ipc.js"),
    windows = require("../src/windows.js"),
    winapi = require("../src/winapi.js");

var teardowns = [];

jqUnit.module("GPII pipe tests", {
    teardown: function () {
        while (teardowns.length) {
            teardowns.pop()();
        }
    }
});

var createLogFile = function () {
    // The child process will write to this file (stdout isn't captured)
    var logFile = os.tmpdir() + "/gpii-test-output" + Date.now();
    console.log("logfile:", logFile);
    fs.writeFileSync(logFile, "");
    teardowns.push(function () {
        try {
            fs.unlinkSync(logFile);
        } catch (e) {
            // Ignored
        }
    });
    return logFile;
};

// Tests generatePipeName
jqUnit.test("Test generatePipeName", function () {
    var pipePrefix = "\\\\.\\pipe\\";

    // Because the names are random, check against a sample of them to avoid lucky results.
    var sampleSize = 300;
    var pipeNames = [];
    for (var n = 0; n < sampleSize; n++) {
        pipeNames.push(gpiiIPC.generatePipeName());
    }

    for (var pipeIndex = 0; pipeIndex < sampleSize; pipeIndex++) {
        var fullName = pipeNames[pipeIndex];

        // Pipe Names: https://msdn.microsoft.com/library/aa365783
        jqUnit.assertTrue("Pipe path must begin with " + pipePrefix, fullName.startsWith(pipePrefix));
        jqUnit.assertTrue("Entire pipe name must <= 256 characters", fullName.length <= 256);

        var pipeName = fullName.substr(pipePrefix.length);
        jqUnit.assertTrue("Pipe name must at least 1 character", pipeName.length > 0);
        // "any character other than a backslash, including numbers and special characters"
        // This also includes '/' because node swaps it with '\'.
        jqUnit.assertFalse("Pipe name must not contain a slash or blackslash", pipeName.match(/[\\\/]/));

        // There shouldn't be any repeated names in a sample size of this size.
        var dup = pipeNames.indexOf(fullName) !== pipeIndex;
        jqUnit.assertFalse("There shouldn't be any repeated pipe names", dup);
    }
});

jqUnit.asyncTest("Test createPipe", function () {
    jqUnit.expect(4);

    var pipeName = gpiiIPC.generatePipeName();

    var promise = gpiiIPC.createPipe(pipeName);
    jqUnit.assertNotNull("createPipe must return non-null", promise);
    jqUnit.assertEquals("createPipe must return a promise", "function", typeof(promise.then));

    promise.then(function (pipeServer) {
        jqUnit.assertTrue("createPipe should have resolved with a value", !!pipeServer);

        jqUnit.assertTrue("createPipe resolved value should be a net.Server instance",
            pipeServer instanceof net.Server);

        jqUnit.start();
    }, function (err) {
        console.error(err);
        jqUnit.fail("createPipe should have resolved");
    });
});

jqUnit.asyncTest("Test createPipe failures", function () {

    var existingPipe = gpiiIPC.generatePipeName();

    var pipeNames = [
        // A pipe that exists.
        existingPipe,
        // Badly formed name
        "invalid",
        null
    ];

    jqUnit.expect(pipeNames.length * 3);

    var testPipes = function (pipeNames) {
        var pipeName = pipeNames.shift();
        console.log("Checking bad pipe name:", pipeName);

        var promise = gpiiIPC.createPipe(pipeName);
        jqUnit.assertNotNull("createPipe must return non-null", promise);
        jqUnit.assertEquals("createPipe must return a promise", "function", typeof(promise.then));

        promise.then(function () {
            jqUnit.fail("createPipe should not have resolved");
        }, function () {
            jqUnit.assert("createPipe should reject");

            if (pipeNames.length > 0) {
                testPipes(pipeNames);
            } else {
                jqUnit.start();
            }
        });
    };

    // Create a pipe to see what happens if another pipe is created with the same name.
    gpiiIPC.createPipe(existingPipe).then(function () {
        // run the tests.
        testPipes(Array.from(pipeNames));
    }, function (err) {
        console.error(err);
        jqUnit.fail("initial createPipe failed");
    });

});

/**
 * Read from a pipe, calling callback with all the data when it ends.
 *
 * @param pipeName {String} Pipe name.
 * @param callback {Function(err,data)} What to call.
 */
function readPipe(pipeName, callback) {
    var buffer = "";
    var server = net.createServer(function (con) {
        con.setEncoding("utf8");
        buffer = "";

        con.on("error", function (err) {
            console.error(err);
            callback(err);
        });
        con.on("data", function (data) {
            buffer += data;
        });
        con.on("end", function () {
            callback(null, buffer);
        });
    });
    server.listen(pipeName);
    server.on("error", function (err) {
        console.error(err);
        jqUnit.fail("Error with the pipe server");
    });
}

// Tests the execution of a child process with gpiiIPC.execute
jqUnit.asyncTest("Test execute", function () {

    jqUnit.expect(4);

    var testData = {
        execOptions: {
            env: {
                "GPII_TEST_VALUE1": "value1",
                "GPII_TEST_VALUE2": "value2"
            },
            currentDir: process.env.APPDATA
        }
    };

    // Create a pipe so the child process can talk back (gpiiIPC.execute doesn't capture the child's stdout).
    var pipeName = gpiiIPC.generatePipeName();
    readPipe(pipeName, checkReturn);

    var options = Object.assign({}, testData.execOptions);

    // Two asserts for each environment value.
    jqUnit.expect(Object.keys(options.env).length * 2);

    // Start the child process, passing the pipe name.
    var script = path.join(__dirname, "gpii-ipc-tests-child.js");
    var command = ["node", script, "named-pipe", pipeName].join(" ");
    console.log("Executing", command);
    var pid = gpiiIPC.execute(command, options);

    jqUnit.assertEquals("execute should return a number", "number", typeof(pid));

    function checkReturn(err, data) {
        if (err) {
            console.error(err);
            jqUnit.fail("The was something wrong with the pipe");
        } else {
            var obj;

            try {
                obj = JSON.parse(data);
            } catch (e) {
                console.log("child returned: ", data);
                throw e;
            }

            jqUnit.assertTrue("child process should return something", !!obj);
            if (obj.error) {
                console.log("Error from child:", obj.error);
                jqUnit.fail("child process returned an error");
                return;
            }
            jqUnit.assertEquals("'currentDir' should return from child", options.currentDir, obj.currentDir);
            jqUnit.assertTrue("'env' should return from child", !!obj.env);

            for (var key in options.env) {
                var value = options.env[key];
                jqUnit.assertTrue("Environment should contain " + key, key, obj.hasOwnProperty(key));
                jqUnit.assertEquals("Environment '" + key + "' should be the expected value", value, obj.env[key]);
            }

            jqUnit.start();
        }
    }

    teardowns.push(function () {
        try {
            process.kill(pid);
        } catch (e) {
            // Ignored.
        }
    });

    // Set a timeout.
    windows.waitForProcessTermination(pid, 5000).then(function (value) {
        if (value === "timeout") {
            jqUnit.fail("Child process took too long.");
        }
    }, function (err) {
        console.error(err);
        jqUnit.fail("Unable to wait for child process.");
    });

});

// Tests validateClient
jqUnit.asyncTest("Test validateClient", function () {

    var tests = [
        {
            // SetEvent is called by this process.
            id: "successful",
            startChildProcess: false,
            setEvent: true,
            timeout: 20,
            expect: {
                end: false,
                resolve: true
            }
        },
        {
            // SetEvent is not called.
            id: "timeout",
            startChildProcess: false,
            setEvent: false,
            timeout: 2,
            expect: {
                end: true,
                resolve: false
            }
        },
        {
            // SetEvent is called by another process (that hasn't been granted the handle).
            id: "other-process",
            startChildProcess: true,
            setEvent: false,
            timeout: 2,
            expect: {
                end: true,
                resolve: false
            }
        }
    ];

    var runTest = function (testIndex) {
        if (testIndex >= tests.length) {
            jqUnit.start();
            return;
        }

        var test = tests[testIndex];
        var endCalled = false;

        // A mock pipe.
        var pipe = {
            write: function (data) {
                var eventHandleText = data.substr("event:".length).trim();
                jqUnit.assertFalse("event handle must be numeric", isNaN(eventHandleText));

                if (test.startChildProcess) {
                    var script = path.join(__dirname, "gpii-ipc-tests-child.js");
                    var command = ["node", script, "validate-client"].join(" ");
                    console.log("starting", command);
                    child_process.exec(command, function (err, stdout, stderr) {
                        console.log("child stdout:", stdout);
                        console.log("child stderr:", stderr);
                        if (err) {
                            jqUnit.fail(err);
                        } else {
                            jqUnit.assertTrue("child should have called SetEvent",
                                stdout.indexOf("SetEvent returned") > -1);
                        }
                    });

                }

                if (test.setEvent) {
                    var eventHandle = parseInt(eventHandleText);
                    winapi.kernel32.SetEvent(eventHandle);
                }
            },
            end: function () {
                endCalled = true;
            }
        };

        var pid = process.pid;
        gpiiIPC.validateClient(pipe, pid, test.timeout).then(function () {
            jqUnit.assertTrue("validateClient resolved", test.expect.resolve);
        }, function (e) {
            console.log(e);
            jqUnit.assertFalse("validateClient rejected", test.expect.resolve);
        }).then(function () {
            jqUnit.assertEquals("end should be called (if expected)", test.expect.end, endCalled);
            runTest(testIndex + 1);
        });
    };
    runTest(0);

});

// Tests startProcess - creates a child process using startProcess.
jqUnit.asyncTest("Test startProcess", function () {
    var logFile = createLogFile();
    var getLog = function () {
        return fs.readFileSync(logFile, {encoding: "utf8"});
    };

    var script = path.join(__dirname, "gpii-ipc-tests-child.js");
    var command = ["node", script, "inherited-pipe", logFile].join(" ");
    console.log("Starting", command);

    // 0: server sends challenge, client performs it, server sends "OK"
    // 1: child sends expected[0]
    // 2: parent sends sendData
    // 3: child sends expected[1]
    var sendData = "FROM PARENT" + Math.random();
    var expected = [
        // Child responds with what was sent (from the authentication).
        "received: OK\n",
        // Child responds with what was sent (after authentication).
        "received: " + sendData + "\n"
    ];

    var expectedIndex = 0;
    var complete = false;
    var allData = "";
    var pid = null;

    var promise = gpiiIPC.startProcess(command, "test-startProcess");

    jqUnit.assertNotNull("startProcess must return non-null", promise);
    jqUnit.assertEquals("startProcess must return a promise", "function", typeof(promise.then));

    promise.then(function (p) {
        jqUnit.assertNotNull("startProcess must return resolve with a value", p);
        jqUnit.assertFalse("startProcess resolved value pid field must be numeric", isNaN(p.pid));
        jqUnit.assertTrue("startProcess resolved value pipeServer field must be a net.Server instance",
            p.pipeServer instanceof net.Server);

        pid = p.pid;

        // Set a timeout.
        windows.waitForProcessTermination(pid, 5000).then(function (value) {
            var logContent = getLog();
            if (value === "timeout") {
                console.log(logContent);
                jqUnit.fail("Child process took too long.");
            } else {
                if (!complete) {
                    console.log(logContent);
                    jqUnit.fail("child should not terminate until completed");
                } else if (logContent.indexOf("FAIL") >= 0) {
                    console.log(logContent);
                    jqUnit.fail("Child process failed");
                }
            }
        }, function (err) {
            console.error(err);
            jqUnit.fail("Unable to wait for child process.");
        });

        p.pipeServer.on("error", function (err) {
            console.error("Pipe server error:", err);
            jqUnit.fail("Pipe server failed");
        });

        // Wait for the child to connect to the pipe
        p.pipeServer.on("connection", function (pipe) {
            console.log("connection from child");

            pipe.on("data", function (data) {
                allData += data;
                if (allData.indexOf("\n") >= 0) {
                    console.log("from pipe:", allData);
                    jqUnit.assertEquals("Expected input from pipe", expected[expectedIndex], allData);
                    allData = "";
                    expectedIndex++;
                    if (expectedIndex >= expected.length) {
                        complete = true;
                        pipe.end();
                    } else {
                        console.log("send: " + sendData);
                        pipe.write(sendData + "\n");
                    }
                }
            });

            pipe.on("end", function () {
                console.log("pipe end");
                if (complete) {
                    jqUnit.start();
                } else {
                    console.log(getLog());
                    jqUnit.fail("child should not terminate until completed");
                }
            });

            pipe.on("error", function (err) {
                console.error("Pipe error:", err);
                jqUnit.fail("Pipe failed.");
            });
        });

    }, jqUnit.fail);

    teardowns.push(function () {
        try {
            process.kill(pid);
        } catch (e) {
            // Ignored.
        }
    });
});
<|endoftext|>