<commit_before>/*global define*/
define(
	function()
	{
		var signals;
		var image;
		var initialized = false;

		function init( shared )
		{
			signals = shared.signals;
			image = new Image();

			signals['set-new-src'].add( setSrc );

			image.addEventListener( 'load', imageLoaded, false );

			// gf: "bikes" by snorpey on flickr:
			// http://flickr.com/photos/snorpey/8700571753/sizes/z/in/photostream/
			setSrc( 'bikes.jpg' );
		}

		function imageLoaded()
		{
			signals['image-loaded'].dispatch( image );
			initialized = true;
		}

		function setSrc( src )
		{
			image.src = src;

			if (
				initialized &&
				image.naturalWidth !== undefined &&
				image.naturalWidth !== 0
			)
			{
				signals['image-loaded'].dispatch( image );
			}
		}

		return { init: init };
	}
);<commit_msg>update comment<commit_after>/*global define*/
define(
	function()
	{
		var signals;
		var image;
		var initialized = false;

		function init( shared )
		{
			signals = shared.signals;
			image = new Image();

			signals['set-new-src'].add( setSrc );

			image.addEventListener( 'load', imageLoaded, false );

			// "bikes" by snorpey on flickr:
			// http://flickr.com/photos/snorpey/8700571753/sizes/z/in/photostream/
			setSrc( 'bikes.jpg' );
		}

		function imageLoaded()
		{
			signals['image-loaded'].dispatch( image );
			initialized = true;
		}

		function setSrc( src )
		{
			image.src = src;

			if (
				initialized &&
				image.naturalWidth !== undefined &&
				image.naturalWidth !== 0
			)
			{
				signals['image-loaded'].dispatch( image );
			}
		}

		return { init: init };
	}
);<|endoftext|>


<commit_before>const path = require('path')
const inherits = require('inherits')
const events = require('events')
const fs = require('fs-extra')
const uniq = require('lodash/uniq')

const debug = require('debug')('sciencefair:paper')

function Paper (data) {
  if (data instanceof Paper) return data
  if (!(this instanceof Paper)) return new Paper(data)
  events.EventEmitter.call(this)

  if (data.document) {
    Object.assign(data, data.document)
    delete data.document
  }

  const self = this
  self.progress = 0
  self.selected = false

  self.loadData = () => {
    self.loadBib()
    self.loadSearchResult()
  }

  self.loadBib = () => {
    self.title = htmlify(data.title || 'no title')
    self.author = data.author || { 'surname': 'unknown', 'given-names': 'unknown' }
    self.abstract = htmlify(data.abstract || 'no abstract given')
    self.date = data.date || { year: 'unknown ', month: 'unknown', day: 'unknown'}
    self.tags = data.tags || []
    self.identifiers = data.identifier || []
    self.source = data.source
    if (!self.source) {
      throw new Error('Paper requires a source:', data)
    }
    self.loadDatasource()
    self.loadID()
  }

  self.loadDatasource = () => {
    // TODO: remove this path hack once hyperdrive is optimised,
    // else have sciencefair datasource generator handle it
    self.entryfile = data.entryfile
    if (!self.entryfile) {
      throw new Error('Paper requires an entryfile', data)
    }
    self.path = path.join('/articles', data.path.split('').join('/'))
    self.files = uniq((data.files || []).concat([data.entryfile])).map(
      file => path.join(self.path, file)
    )
    require('./getdatasource').fetch(self.source, (err, ds) => {
      if (err)  return cb(err)
      self.ds = ds
      self.emit('datasource:loaded', ds)
    })
  }

  self.loadSearchResult = () => {
    if (data.score) self.score = data.score
  }

  self.loadID = () => {
    // if (data.key) self.key = data.key
    var doi = self.identifiers.find(id => id.type.toLowerCase() === 'doi')
    self.id = doi ? doi.id : self.identifiers[0].id
    self.key = `${self.source}:${self.id}`
  }

  self.filesPresent = cb => {
    if (self.downloading) return cb(null, self.progress)
    if (self.progress === 100) return cb(null, self.progress)
    if (self.progresschecked) return cb(null, self.progress)
    if (!(self.ds && self.ds.articles && self.ds.articles.content)) {
      // datasource not ready to check progress, try again after delay
      return setTimeout(() => self.filesPresent(cb), 500)
    }
    self.ds.articlestats(self.files, (err, stats) => {
      if (err) return cb(err)
      debug('progress stats', self.title, stats)
      if (stats.progress > 0) self.collected = true
      self.progress = stats.progress * 100
      self.emit('progress', self.minprogress())
      self.progresschecked = true
      cb(null, self.progress, true)
    })
  }

  self.candownload = () => self.ds.ready()

  self.download = () => {
    if (self.downloading) return null
    const download = self.ds.download(self)
    if (!download) return null // datasource not ready
    debug('downloading', self.key)
    self.emit('download:started')
    self.collected = true
    self.downloading = true

    self.emit('progress', self.minprogress())

    const done = () => {
      if (!self.downloading) return
      debug('downloaded', self.key)
      self.downloading = false
      self.progresschecked = true
      self.emit('download:done')
    }

    download.on('progress', data => {
      self.progress = data.progress * 100
      self.emit('progress', self.minprogress())
      if (self.progress === 100) done()
    })

    download.on('error', err => {
      self.downloading = false
      debug('error downloading paper: ', self.key)
      self.emit('download:error', err)
    })

    download.on('end', done)

    return download
  }

  self.metadata = () => {
    return {
      title: self.title,
      author: self.author,
      authorstr: self.author.map(a => `${a['given-names']} ${a.surname}`).join(' '),
      date: self.date,
      abstract: self.abstract,
      tags: self.tags,
      source: self.source,
      id: self.id,
      key: self.key,
      identifier: self.identifiers,
      path: data.path,
      files: data.files,
      entryfile: self.entryfile
    }
  }

  self.removeFiles = cb => {
    const done = err => {
      if (err) return cb(err)
      self.progress = 0
      self.downloading = false
      self.progresschecked = true
      self.emit('cleared')
      self.emit('progress', 0)
      cb()
    }

    self.ds.clear(self.files, done)
  }

  self.minprogress = () => {
    return self.downloading ? Math.max(self.progress, 10) : self.progress
  }

  self.select = () => {
    self.selected = true
    self.emit('selected')
  }

  self.deselect = () => {
    self.selected = false
    self.emit('deselected')
  }

  self.loadData()
}

inherits(Paper, events.EventEmitter)

function htmlify (str) {
  return str.replace('<italic>', '').replace('</italic>', '')
}

module.exports = Paper
<commit_msg>Real fix for author issue.<commit_after>const path = require('path')
const inherits = require('inherits')
const events = require('events')
const fs = require('fs-extra')
const uniq = require('lodash/uniq')

const debug = require('debug')('sciencefair:paper')

function Paper (data) {
  if (data instanceof Paper) return data
  if (!(this instanceof Paper)) return new Paper(data)
  events.EventEmitter.call(this)

  if (data.document) {
    Object.assign(data, data.document)
    delete data.document
  }

  const self = this
  self.progress = 0
  self.selected = false

  self.loadData = () => {
    self.loadBib()
    self.loadSearchResult()
  }

  self.loadBib = () => {
    self.title = htmlify(data.title || 'no title')
    self.author = data.author || [{ 'surname': 'unknown', 'given-names': 'unknown' }]
    self.abstract = htmlify(data.abstract || 'no abstract given')
    self.date = data.date || { year: 'unknown ', month: 'unknown', day: 'unknown'}
    self.tags = data.tags || []
    self.identifiers = data.identifier || []
    self.source = data.source
    if (!self.source) {
      throw new Error('Paper requires a source:', data)
    }
    self.loadDatasource()
    self.loadID()
  }

  self.loadDatasource = () => {
    // TODO: remove this path hack once hyperdrive is optimised,
    // else have sciencefair datasource generator handle it
    self.entryfile = data.entryfile
    if (!self.entryfile) {
      throw new Error('Paper requires an entryfile', data)
    }
    self.path = path.join('/articles', data.path.split('').join('/'))
    self.files = uniq((data.files || []).concat([data.entryfile])).map(
      file => path.join(self.path, file)
    )
    require('./getdatasource').fetch(self.source, (err, ds) => {
      if (err)  return cb(err)
      self.ds = ds
      self.emit('datasource:loaded', ds)
    })
  }

  self.loadSearchResult = () => {
    if (data.score) self.score = data.score
  }

  self.loadID = () => {
    // if (data.key) self.key = data.key
    var doi = self.identifiers.find(id => id.type.toLowerCase() === 'doi')
    self.id = doi ? doi.id : self.identifiers[0].id
    self.key = `${self.source}:${self.id}`
  }

  self.filesPresent = cb => {
    if (self.downloading) return cb(null, self.progress)
    if (self.progress === 100) return cb(null, self.progress)
    if (self.progresschecked) return cb(null, self.progress)
    if (!(self.ds && self.ds.articles && self.ds.articles.content)) {
      // datasource not ready to check progress, try again after delay
      return setTimeout(() => self.filesPresent(cb), 500)
    }
    self.ds.articlestats(self.files, (err, stats) => {
      if (err) return cb(err)
      debug('progress stats', self.title, stats)
      if (stats.progress > 0) self.collected = true
      self.progress = stats.progress * 100
      self.emit('progress', self.minprogress())
      self.progresschecked = true
      cb(null, self.progress, true)
    })
  }

  self.candownload = () => self.ds.ready()

  self.download = () => {
    if (self.downloading) return null
    const download = self.ds.download(self)
    if (!download) return null // datasource not ready
    debug('downloading', self.key)
    self.emit('download:started')
    self.collected = true
    self.downloading = true

    self.emit('progress', self.minprogress())

    const done = () => {
      if (!self.downloading) return
      debug('downloaded', self.key)
      self.downloading = false
      self.progresschecked = true
      self.emit('download:done')
    }

    download.on('progress', data => {
      self.progress = data.progress * 100
      self.emit('progress', self.minprogress())
      if (self.progress === 100) done()
    })

    download.on('error', err => {
      self.downloading = false
      debug('error downloading paper: ', self.key)
      self.emit('download:error', err)
    })

    download.on('end', done)

    return download
  }

  self.metadata = () => {
    return {
      title: self.title,
      author: self.author,
      authorstr: self.author.map(a => `${a['given-names']} ${a.surname}`).join(' '),
      date: self.date,
      abstract: self.abstract,
      tags: self.tags,
      source: self.source,
      id: self.id,
      key: self.key,
      identifier: self.identifiers,
      path: data.path,
      files: data.files,
      entryfile: self.entryfile
    }
  }

  self.removeFiles = cb => {
    const done = err => {
      if (err) return cb(err)
      self.progress = 0
      self.downloading = false
      self.progresschecked = true
      self.emit('cleared')
      self.emit('progress', 0)
      cb()
    }

    self.ds.clear(self.files, done)
  }

  self.minprogress = () => {
    return self.downloading ? Math.max(self.progress, 10) : self.progress
  }

  self.select = () => {
    self.selected = true
    self.emit('selected')
  }

  self.deselect = () => {
    self.selected = false
    self.emit('deselected')
  }

  self.loadData()
}

inherits(Paper, events.EventEmitter)

function htmlify (str) {
  return str.replace('<italic>', '').replace('</italic>', '')
}

module.exports = Paper
<|endoftext|>


<commit_before>import React from 'react';
import ReactDOM from 'react-dom';
import 'raf/polyfill';
import { ApolloProvider } from 'react-apollo';

// eslint-disable-next-line import/extensions
import UniversalRouter from 'universal-router';
import queryString from 'query-string';
import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import bowser from 'bowser';

import RedBox from 'redbox-react';

import App from 'components/App';

import configureStore from 'store/configureStore';
import apolloClient from 'store/apolloClient';

import { defaultCampaignStartState, defaultActionStartState } from 'reducers/SearchReducer';

import { checkSessionStatus } from 'actions/AuthActions';
import { startPageLoad, endPageLoad, attemptNavFromDirtyForm } from 'actions/NotificationsActions';

import ActivitiesQuery from 'schemas/queries/ActivitiesQuery.graphql';

import history from 'lib/history';
import ReactGA from 'lib/react-ga';
import routes from 'routes';

import { sentryDsn, googleUA } from 'config/config';

import Raven from 'raven-js';

Raven.config(sentryDsn).install();

const container = document.getElementById('app');

if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') {
  // Delay registration until after the page has loaded, to ensure that our
  // precaching requests don't degrade the first visit experience.
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('./service-worker.js')
      .then((reg) => {
        // updatefound is fired if service-worker.js changes.
        // eslint-disable-next-line no-param-reassign
        reg.onupdatefound = () => {
          const installingWorker = reg.installing;

          installingWorker.onstatechange = () => {
            switch (installingWorker.state) {
              case 'installed':
                if (navigator.serviceWorker.controller) {
                  // At this point, the old content will have been purged and the fresh content will
                  // have been added to the cache.
                  // TODO: sure we need to do a hard reload here?
                  window.setTimeout(() => {
                    window.location.reload(true);
                  }, 100);
                }
                break;

              case 'redundant':
                console.error('The installing service worker became redundant.');
                break;

              default:
                break;
            }
          };
        };
      })
      .catch((e) => {
        console.error('Error during service worker registration:', e);
      });
  });
}

const store = configureStore({
  actionsSearch: defaultActionStartState,
  campaignsSearch: defaultCampaignStartState,
});

// eslint-disable-next-line no-unused-vars
let appInstance;
let currentLocation = history.location;

// Switch off the native scroll restoration behavior and handle it manually
// https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration
const scrollPositionsHistory = {};
if (window.history && 'scrollRestoration' in window.history) {
  window.history.scrollRestoration = 'manual';
}

// Global (context) variables that can be easily accessed from any React component
// https://facebook.github.io/react/docs/context.html
const context = {};

const onRenderComplete = function initialRenderComplete(route, location) {
  store.dispatch(endPageLoad());

  let scrollX = 0;
  let scrollY = 0;

  const pos = scrollPositionsHistory[location.key];
  if (pos) {
    ({ scrollX, scrollY } = pos);
  } else {
    const targetHash = location.hash.substr(1);
    if (targetHash) {
      const target = document.getElementById(targetHash);
      if (target) {
        scrollY = window.pageYOffset + target.getBoundingClientRect().top;
      }
    }
  }

  // Restore the scroll position if it was saved into the state
  // or scroll to top of the page
  window.setTimeout(() => {
    window.scrollTo(scrollX, scrollY);
  }, 10);
};

// Re-render the app when window.location changes
async function onLocationChange(location) {
  const isInitialRender = !history.action;

  try {
    // TODO: Not only does this seem to occasionally get stuck, it also just reeks of code smell
    const fetchingSessionStatus = store.getState().userAuthSession.fetchingAuthUpdate;
    if (!fetchingSessionStatus) {
      store.dispatch(checkSessionStatus());
    }

    // Remember the latest scroll position for the previous location
    scrollPositionsHistory[currentLocation.key] = {
      scrollX: window.pageXOffset,
      scrollY: window.pageYOffset,
    };

    // Delete stored scroll position for next page if any
    if (history.action === 'PUSH') {
      delete scrollPositionsHistory[location.key];
    }

    currentLocation = location;

    store.dispatch(startPageLoad());

    // Traverses the list of routes in the order they are defined until
    // it finds the first route that matches provided URL path string
    // and whose action method returns anything other than `undefined`.
    const router = new UniversalRouter(routes);
    const route = await router.resolve({
      pathname: location.pathname,
      query: queryString.parse(location.search),
      apolloClient,
      store, // be wary of using the store in routing considering async updates
    });

    // Prevent multiple page renders during the routing process
    if (currentLocation.key !== location.key) {
      return;
    }

    if (route.redirect) {
      history.replace(route.redirect);
      return;
    }

    const newTitle = route.title;
    if (document.title !== newTitle) {
      document.title = newTitle;
    }

    console.log('ga', googleUA);
    if (googleUA) {
      ReactGA.initialize(googleUA, { debug: true });
    }

    ReactGA.set({ page: location.pathname });
    ReactGA.pageview(location.pathname);

    // Hack for samsung browsers which aren't well suported by the
    // inline-style-prefixer library that material-ui uses
    const userAgent = bowser.samsungBrowser ? 'all' : undefined;

    appInstance = ReactDOM.render(
      <MuiThemeProvider muiTheme={getMuiTheme(lightBaseTheme, { userAgent })}>
        <ApolloProvider client={apolloClient} store={store}>
          <App
            pageTitle={route.pageTitle}
            title={route.title}
            description={route.description}
            context={context}
            url={window.location.href}
            image={route.image}
          >
            {route.component}
          </App>
        </ApolloProvider>
      </MuiThemeProvider>,
      container,
      () => onRenderComplete(route, location),
    );
  } catch (error) {
    Raven.captureException(error, {
      extra: {
        state: store.getState(),
      },
    });

    window.console && console.error && console.error(error);

    // Current url has been changed during navigation process, do nothing
    if (currentLocation.key !== location.key) {
      return;
    }

    // Display the error in full-screen for development mode
    if (process.env.NODE_ENV !== 'production') {
      appInstance = null;
      document.title = `Error: ${error.message}`;
      ReactDOM.render(<RedBox error={error} />, container);
      return;
    }

    // Do a full page reload if error occurs during client-side navigation
    if (!isInitialRender && currentLocation.key === location.key) {
      // TODO: Proper response during production error
    }
  }
}

history.block((location, action) => {
  if (!store.getState().notifications.formStateClean) {
    store.dispatch(attemptNavFromDirtyForm(location.pathname));
    // This just needs to return a string in order to tell the history API to block
    return 'unsaved changes';
  }
  return undefined;
});

// Handle client-side navigation by using HTML5 History API
// For more information visit https://github.com/mjackson/history#readme
history.listen(onLocationChange);
onLocationChange(currentLocation);

// We can prefetch these basic queries because they're integral to UI performance
apolloClient.query({ query: ActivitiesQuery });
<commit_msg>Remove debug info<commit_after>import React from 'react';
import ReactDOM from 'react-dom';
import 'raf/polyfill';
import { ApolloProvider } from 'react-apollo';

// eslint-disable-next-line import/extensions
import UniversalRouter from 'universal-router';
import queryString from 'query-string';
import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import bowser from 'bowser';

import RedBox from 'redbox-react';

import App from 'components/App';

import configureStore from 'store/configureStore';
import apolloClient from 'store/apolloClient';

import { defaultCampaignStartState, defaultActionStartState } from 'reducers/SearchReducer';

import { checkSessionStatus } from 'actions/AuthActions';
import { startPageLoad, endPageLoad, attemptNavFromDirtyForm } from 'actions/NotificationsActions';

import ActivitiesQuery from 'schemas/queries/ActivitiesQuery.graphql';

import history from 'lib/history';
import ReactGA from 'lib/react-ga';
import routes from 'routes';

import { sentryDsn, googleUA } from 'config/config';

import Raven from 'raven-js';

Raven.config(sentryDsn).install();

const container = document.getElementById('app');

if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') {
  // Delay registration until after the page has loaded, to ensure that our
  // precaching requests don't degrade the first visit experience.
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('./service-worker.js')
      .then((reg) => {
        // updatefound is fired if service-worker.js changes.
        // eslint-disable-next-line no-param-reassign
        reg.onupdatefound = () => {
          const installingWorker = reg.installing;

          installingWorker.onstatechange = () => {
            switch (installingWorker.state) {
              case 'installed':
                if (navigator.serviceWorker.controller) {
                  // At this point, the old content will have been purged and the fresh content will
                  // have been added to the cache.
                  // TODO: sure we need to do a hard reload here?
                  window.setTimeout(() => {
                    window.location.reload(true);
                  }, 100);
                }
                break;

              case 'redundant':
                console.error('The installing service worker became redundant.');
                break;

              default:
                break;
            }
          };
        };
      })
      .catch((e) => {
        console.error('Error during service worker registration:', e);
      });
  });
}

const store = configureStore({
  actionsSearch: defaultActionStartState,
  campaignsSearch: defaultCampaignStartState,
});

// eslint-disable-next-line no-unused-vars
let appInstance;
let currentLocation = history.location;

// Switch off the native scroll restoration behavior and handle it manually
// https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration
const scrollPositionsHistory = {};
if (window.history && 'scrollRestoration' in window.history) {
  window.history.scrollRestoration = 'manual';
}

// Global (context) variables that can be easily accessed from any React component
// https://facebook.github.io/react/docs/context.html
const context = {};

const onRenderComplete = function initialRenderComplete(route, location) {
  store.dispatch(endPageLoad());

  let scrollX = 0;
  let scrollY = 0;

  const pos = scrollPositionsHistory[location.key];
  if (pos) {
    ({ scrollX, scrollY } = pos);
  } else {
    const targetHash = location.hash.substr(1);
    if (targetHash) {
      const target = document.getElementById(targetHash);
      if (target) {
        scrollY = window.pageYOffset + target.getBoundingClientRect().top;
      }
    }
  }

  // Restore the scroll position if it was saved into the state
  // or scroll to top of the page
  window.setTimeout(() => {
    window.scrollTo(scrollX, scrollY);
  }, 10);
};

// Re-render the app when window.location changes
async function onLocationChange(location) {
  const isInitialRender = !history.action;

  try {
    // TODO: Not only does this seem to occasionally get stuck, it also just reeks of code smell
    const fetchingSessionStatus = store.getState().userAuthSession.fetchingAuthUpdate;
    if (!fetchingSessionStatus) {
      store.dispatch(checkSessionStatus());
    }

    // Remember the latest scroll position for the previous location
    scrollPositionsHistory[currentLocation.key] = {
      scrollX: window.pageXOffset,
      scrollY: window.pageYOffset,
    };

    // Delete stored scroll position for next page if any
    if (history.action === 'PUSH') {
      delete scrollPositionsHistory[location.key];
    }

    currentLocation = location;

    store.dispatch(startPageLoad());

    // Traverses the list of routes in the order they are defined until
    // it finds the first route that matches provided URL path string
    // and whose action method returns anything other than `undefined`.
    const router = new UniversalRouter(routes);
    const route = await router.resolve({
      pathname: location.pathname,
      query: queryString.parse(location.search),
      apolloClient,
      store, // be wary of using the store in routing considering async updates
    });

    // Prevent multiple page renders during the routing process
    if (currentLocation.key !== location.key) {
      return;
    }

    if (route.redirect) {
      history.replace(route.redirect);
      return;
    }

    const newTitle = route.title;
    if (document.title !== newTitle) {
      document.title = newTitle;
    }

    if (googleUA) {
      ReactGA.initialize(googleUA);
    }

    ReactGA.set({ page: location.pathname });
    ReactGA.pageview(location.pathname);

    // Hack for samsung browsers which aren't well suported by the
    // inline-style-prefixer library that material-ui uses
    const userAgent = bowser.samsungBrowser ? 'all' : undefined;

    appInstance = ReactDOM.render(
      <MuiThemeProvider muiTheme={getMuiTheme(lightBaseTheme, { userAgent })}>
        <ApolloProvider client={apolloClient} store={store}>
          <App
            pageTitle={route.pageTitle}
            title={route.title}
            description={route.description}
            context={context}
            url={window.location.href}
            image={route.image}
          >
            {route.component}
          </App>
        </ApolloProvider>
      </MuiThemeProvider>,
      container,
      () => onRenderComplete(route, location),
    );
  } catch (error) {
    Raven.captureException(error, {
      extra: {
        state: store.getState(),
      },
    });

    window.console && console.error && console.error(error);

    // Current url has been changed during navigation process, do nothing
    if (currentLocation.key !== location.key) {
      return;
    }

    // Display the error in full-screen for development mode
    if (process.env.NODE_ENV !== 'production') {
      appInstance = null;
      document.title = `Error: ${error.message}`;
      ReactDOM.render(<RedBox error={error} />, container);
      return;
    }

    // Do a full page reload if error occurs during client-side navigation
    if (!isInitialRender && currentLocation.key === location.key) {
      // TODO: Proper response during production error
    }
  }
}

history.block((location, action) => {
  if (!store.getState().notifications.formStateClean) {
    store.dispatch(attemptNavFromDirtyForm(location.pathname));
    // This just needs to return a string in order to tell the history API to block
    return 'unsaved changes';
  }
  return undefined;
});

// Handle client-side navigation by using HTML5 History API
// For more information visit https://github.com/mjackson/history#readme
history.listen(onLocationChange);
onLocationChange(currentLocation);

// We can prefetch these basic queries because they're integral to UI performance
apolloClient.query({ query: ActivitiesQuery });
<|endoftext|>


<commit_before>// @flow
import message from '@conveyal/woonerf/message'
import {color} from 'd3-color'
import {format} from 'd3-format'
import React, {PureComponent} from 'react'
import memoize from 'lodash/memoize'

import {Button, Group as ButtonGroup} from '../buttons'
import colors from '../../constants/colors'

import StackedPercentile from './stacked-percentile'

export const PROJECT = 'project'
export const BASE = 'base'
export const COMPARISON = 'comparison'

const GRAPH_HEIGHT = 300
const GRAPH_WIDTH = 600

/**
 * A component allowing toggling between up to two stacked percentile plots and
 * comparisons of said
 */
export default class StackedPercentileSelector extends PureComponent {
  props: {
    accessibility?: number,
    comparisonAccessibility?: number,
    comparisonInProgress: boolean,
    comparisonPercentileCurves?: number[][],
    comparisonProjectName?: string,
    disabled: boolean,
    isochroneCutoff: number,
    maxAccessibility: number,
    nearestPercentile: number,
    opportunityDatasetName?: string,
    percentileCurves: number[][],
    projectName: string,
    setIsochroneCutoff: (cutoff: number) => void,
    stale: boolean
  }

  state: {
    scenarioPlotted: 'project' | 'base' | 'comparison'
  }

  state = {
    scenarioPlotted: PROJECT
  }

  _setIsochroneCutoff = (e: Event & {target: HTMLInputElement}) =>
    this.props.setIsochroneCutoff(parseInt(e.target.value))

  _selectView = memoize((scenarioPlotted: string) => () => this.setState({scenarioPlotted}))

  render () {
    const {
      accessibility,
      comparisonAccessibility,
      comparisonInProgress,
      comparisonPercentileCurves,
      comparisonProjectName,
      disabled,
      opportunityDatasetName,
      isochroneCutoff,
      maxAccessibility,
      nearestPercentile,
      percentileCurves,
      projectName,
      stale
    } = this.props
    const {scenarioPlotted} = this.state
    const noComparison = !comparisonProjectName

    const commaFormat = format(',d')

    const projectColor = disabled || stale
      ? colors.STALE_PERCENTILE_COLOR
      : colors.PROJECT_PERCENTILE_COLOR
    const comparisonColor = disabled || stale
      ? colors.STALE_PERCENTILE_COLOR
      : colors.COMPARISON_PERCENTILE_COLOR

    const colorBar = color(projectColor)
    colorBar.opacity = 0.3
    const comparisonColorBar = color(comparisonColor)
    comparisonColorBar.opacity = 0.3

    return (

Accessibility to {opportunityDatasetName || `[select opportunity dataset]`} in {isochroneCutoff} minutes (travel time percentile: {nearestPercentile}th)

        <input
          className='form-control'
          disabled={disabled}
          type='range'
          value={isochroneCutoff}
          min={1}
          max={120}
          title={message('analysis.cutoff')}
          onChange={this._setIsochroneCutoff}
          style={{
            boxShadow: 'none',
            width: 568,
            position: 'relative',
            left: 46
          }}
        />

        {accessibility !== null &&

            <div
              className='progress-bar'
              role='progressbar'
              style={{
                backgroundColor: colorBar.toString(),
                width: `${(accessibility || 1) / maxAccessibility * 100}%`
              }}
            >

{projectName}

              {commaFormat(accessibility)}

}

        {comparisonInProgress &&
          comparisonProjectName &&
          comparisonAccessibility != null &&

            <div
              className='progress-bar'
              role='progressbar'
              style={{
                backgroundColor: comparisonColorBar.toString(),
                width: `${comparisonAccessibility / maxAccessibility * 100}%`
              }}
            >

                {`${comparisonProjectName}`}

              {commaFormat(comparisonAccessibility)}

}

        {accessibility !== null &&
          <ButtonGroup justified>
            <Button
              active={noComparison || scenarioPlotted === PROJECT}
              disabled={noComparison}
              onClick={this._selectView(PROJECT)}
              size='sm'
              value={PROJECT}
            >
              {projectName}
            </Button>
            <Button
              active={!noComparison && scenarioPlotted === BASE}
              disabled={noComparison}
              onClick={this._selectView(BASE)}
              size='sm'
            >
              {comparisonProjectName || 'No comparison selected'}
            </Button>
            <Button
              active={!noComparison && scenarioPlotted === COMPARISON}
              disabled={noComparison}
              onClick={this._selectView(COMPARISON)}
              size='sm'
            >
              {message('analysis.comparison')}
            </Button>
          </ButtonGroup>}

        {percentileCurves &&
          <StackedPercentile
            percentileCurves={percentileCurves}
            comparisonPercentileCurves={comparisonPercentileCurves}
            width={GRAPH_WIDTH}
            height={GRAPH_HEIGHT}
            isochroneCutoff={isochroneCutoff}
            opportunityDatasetName={opportunityDatasetName}
            textColor={'#333333'}
            color={projectColor}
            comparisonColor={comparisonColor}
            maxAccessibility={maxAccessibility}
            selected={comparisonPercentileCurves ? scenarioPlotted : PROJECT}
            label={projectName}
            comparisonLabel={comparisonProjectName}
          />}

    )
  }
}
<commit_msg>fix(comparisons): Check whether comparison analysis present<commit_after>// @flow
import message from '@conveyal/woonerf/message'
import {color} from 'd3-color'
import {format} from 'd3-format'
import React, {PureComponent} from 'react'
import memoize from 'lodash/memoize'

import {Button, Group as ButtonGroup} from '../buttons'
import colors from '../../constants/colors'
import UNDEFINED_PROJECT_NAME from '../../constants'

import StackedPercentile from './stacked-percentile'

export const PROJECT = 'project'
export const BASE = 'base'
export const COMPARISON = 'comparison'

const GRAPH_HEIGHT = 300
const GRAPH_WIDTH = 600

/**
 * A component allowing toggling between up to two stacked percentile plots and
 * comparisons of said
 */
export default class StackedPercentileSelector extends PureComponent {
  props: {
    accessibility?: number,
    comparisonAccessibility?: number,
    comparisonInProgress: boolean,
    comparisonPercentileCurves?: number[][],
    comparisonProjectName?: string,
    disabled: boolean,
    isochroneCutoff: number,
    maxAccessibility: number,
    nearestPercentile: number,
    opportunityDatasetName?: string,
    percentileCurves: number[][],
    projectName: string,
    setIsochroneCutoff: (cutoff: number) => void,
    stale: boolean
  }

  state: {
    scenarioPlotted: 'project' | 'base' | 'comparison'
  }

  state = {
    scenarioPlotted: PROJECT
  }

  _setIsochroneCutoff = (e: Event & {target: HTMLInputElement}) =>
    this.props.setIsochroneCutoff(parseInt(e.target.value))

  _selectView = memoize((scenarioPlotted: string) => () => this.setState({scenarioPlotted}))

  render () {
    const {
      accessibility,
      comparisonAccessibility,
      comparisonInProgress,
      comparisonPercentileCurves,
      comparisonProjectName,
      disabled,
      opportunityDatasetName,
      isochroneCutoff,
      maxAccessibility,
      nearestPercentile,
      percentileCurves,
      projectName,
      stale
    } = this.props
    const {scenarioPlotted} = this.state
    const noComparison = !comparisonProjectName &&
      comparisonProjectName !== UNDEFINED_PROJECT_NAME

    const commaFormat = format(',d')

    const projectColor = disabled || stale
      ? colors.STALE_PERCENTILE_COLOR
      : colors.PROJECT_PERCENTILE_COLOR
    const comparisonColor = disabled || stale
      ? colors.STALE_PERCENTILE_COLOR
      : colors.COMPARISON_PERCENTILE_COLOR

    const colorBar = color(projectColor)
    colorBar.opacity = 0.3
    const comparisonColorBar = color(comparisonColor)
    comparisonColorBar.opacity = 0.3

    return (

Accessibility to {opportunityDatasetName || `[select opportunity dataset]`} in {isochroneCutoff} minutes (travel time percentile: {nearestPercentile}th)

        <input
          className='form-control'
          disabled={disabled}
          type='range'
          value={isochroneCutoff}
          min={1}
          max={120}
          title={message('analysis.cutoff')}
          onChange={this._setIsochroneCutoff}
          style={{
            boxShadow: 'none',
            width: 568,
            position: 'relative',
            left: 46
          }}
        />

        {accessibility !== null &&

            <div
              className='progress-bar'
              role='progressbar'
              style={{
                backgroundColor: colorBar.toString(),
                width: `${(accessibility || 1) / maxAccessibility * 100}%`
              }}
            >

{projectName}

              {commaFormat(accessibility)}

}

        {comparisonInProgress &&
          comparisonProjectName &&
          comparisonAccessibility != null &&

            <div
              className='progress-bar'
              role='progressbar'
              style={{
                backgroundColor: comparisonColorBar.toString(),
                width: `${comparisonAccessibility / maxAccessibility * 100}%`
              }}
            >

                {`${comparisonProjectName}`}

              {commaFormat(comparisonAccessibility)}

}

        {comparisonAccessibility !== null &&
          <ButtonGroup justified>
            <Button
              active={noComparison || scenarioPlotted === PROJECT}
              disabled={noComparison}
              onClick={this._selectView(PROJECT)}
              size='sm'
              value={PROJECT}
            >
              {projectName}
            </Button>
            <Button
              active={!noComparison && scenarioPlotted === BASE}
              disabled={noComparison}
              onClick={this._selectView(BASE)}
              size='sm'
            >
              {comparisonProjectName || 'No comparison selected'}
            </Button>
            <Button
              active={!noComparison && scenarioPlotted === COMPARISON}
              disabled={noComparison}
              onClick={this._selectView(COMPARISON)}
              size='sm'
            >
              {message('analysis.comparison')}
            </Button>
          </ButtonGroup>}

        {percentileCurves &&
          <StackedPercentile
            percentileCurves={percentileCurves}
            comparisonPercentileCurves={comparisonPercentileCurves}
            width={GRAPH_WIDTH}
            height={GRAPH_HEIGHT}
            isochroneCutoff={isochroneCutoff}
            opportunityDatasetName={opportunityDatasetName}
            textColor={'#333333'}
            color={projectColor}
            comparisonColor={comparisonColor}
            maxAccessibility={maxAccessibility}
            selected={comparisonPercentileCurves ? scenarioPlotted : PROJECT}
            label={projectName}
            comparisonLabel={comparisonProjectName}
          />}

    )
  }
}
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import { Input, Button } from 'react-bootstrap';

export default class Timer extends Component {
  state = {
    secondsRemaining: 60,
    selectedSeconds: 60,
    timerRunning: false
  }
  setInitialTime = (event) => {
    this.setState({selectedSeconds: event.target.options[event.target.selectedIndex].value});
    this.setState({secondsRemaining: event.target.options[event.target.selectedIndex].value});
  }
  startCoundown = () => {
    console.log("Start, timer state: ", this.state.timerRunning);
    if(!(this.state.timerRunning)) {
      console.log("Timer state false, set to true.");
      this.setState({timerRunning: true});
      this.interval = setInterval(this.tick, 1000);
    } else {
      console.log("Timer already running.");
    }
  }
  tick = () => {
    this.setState({secondsRemaining: this.state.secondsRemaining - 1});
    if (this.state.secondsRemaining <= 0) {
      clearInterval(this.interval);
      this.setState({timerRunning: false, secondsRemaining: this.state.selectedSeconds});
    }
  }
	render = () => {
    let button;
    if(!(this.state.timerRunning)) {
      button = <Button block onClick={this.startCoundown}>Start</Button>;
    } else {
      button = <Button block disabled>{this.state.secondsRemaining}</Button>;
    }
		return (

Stop slacking! Time your breaks!

              <Input type="select" value={this.state.selectedSeconds} onChange={this.setInitialTime}>
                <option value="60">60 seconds</option>
                <option value="120">120 seconds</option>
                <option value="180">180 seconds</option>
              </Input>
              {button}

		);
	}
}
<commit_msg>Fixed bug where timer layout broke.<commit_after>import React, { Component } from 'react';
import { Input, Button } from 'react-bootstrap';

export default class Timer extends Component {
  state = {
    secondsRemaining: 60,
    selectedSeconds: 60,
    timerRunning: false
  }
  setInitialTime = (event) => {
    this.setState({selectedSeconds: event.target.options[event.target.selectedIndex].value});
    this.setState({secondsRemaining: event.target.options[event.target.selectedIndex].value});
  }
  startCoundown = () => {
    console.log("Start, timer state: ", this.state.timerRunning);
    if(!(this.state.timerRunning)) {
      console.log("Timer state false, set to true.");
      this.setState({timerRunning: true});
      this.interval = setInterval(this.tick, 1000);
    } else {
      console.log("Timer already running.");
    }
  }
  tick = () => {
    this.setState({secondsRemaining: this.state.secondsRemaining - 1});
    if (this.state.secondsRemaining <= 0) {
      clearInterval(this.interval);
      this.setState({timerRunning: false, secondsRemaining: this.state.selectedSeconds});
    }
  }
	render = () => {
    let button;
    if(!(this.state.timerRunning)) {
      button = <Button block onClick={this.startCoundown}>Start</Button>;
    } else {
      button = <Button block disabled>{this.state.secondsRemaining}</Button>;
    }
		return (

Stop slacking! Time your breaks!

          <Input type="select" value={this.state.selectedSeconds} onChange={this.setInitialTime}>
            <option value="60">60 seconds</option>
            <option value="120">120 seconds</option>
            <option value="180">180 seconds</option>
          </Input>
          {button}

		);
	}
}
<|endoftext|>


<commit_before>/* Lambda function to query the Coursera API and pull the latest OnDemand
 * courses.
 *
 * This function does the following:
 *
 * - Queries the Coursera API to retrieve all On Demand courses
 *
 * - Updates a cached list of On Demand courses on S3. (CACHED_ONDEMAND_KEY)
 *
 * - Queries the Coursera API to check if any previously unlaunched On Demand
 *   courses have been launched, and adds them to the cached list of launched
 *   On Demand courses. (CACHED_ONDEMAND_LAUNCHED_KEY)
 *
 * - Sends a notification email if any new On Demand courses are added or
 *   launched
 *
 * TESTING
 * -------
 * Check the file test-end-to-end.js.
 *
 * We set a special boolean property `isMock` inside the event object passed
 * into the handler, which triggers offline behavior at certain places.
 */

import AWS from 'aws-sdk'
import request from 'request-json'
import _ from 'lodash'
import async from 'async'
import fs from 'fs'
import util from 'util'
import moment from 'moment'

const S3_BUCKET = 'moocfetcher'
const CACHED_ONDEMAND_LAUNCHED_KEY = 'coursera/ondemand/launched.json'
const CACHED_ONDEMAND_KEY = 'coursera/ondemand/all.json'
const COURSERA_API_COURSES_PATH = '/api/courses.v1'
const COURSERA_API_ONDEMAND_PATH = '/api/onDemandCourses.v1?&q=slug&slug=%s'
const client = request.createClient('https://www.coursera.org')
const s3Client = new AWS.S3()
const sesClient = new AWS.SES({region: 'us-east-1'})

exports.handler = function(event, context) {
  // Test function to load courses from a JSON file.
  function loadCoursesFromFile(f) {
    return function(callback) {
      console.log('Fetching courses from file %s…', f)
      callback(null, JSON.parse(fs.readFileSync(f)).courses)
    }
  }

  // Loads all courses by querying the Coursera API.
  function loadFromCoursera(callback) {
    console.log('Fetching courses from Coursera…')
    client.get(COURSERA_API_COURSES_PATH, function(err, res, body) {
      if (!err) {
        if (res.statusCode !== 200) {
          console.log('Unexpected Status Code %s', res.statusCode)
          callback(new Error(util.format('Unexpected Status Code %s', res.statusCode)))
        } else {
          callback(null, body.elements)
        }
      } else {
        console.log('Error fetching courses from Coursera: %s', err)
        callback(err)
      }
    })
  }

  // Loads a list of cached courses from S3, given the key.
  function loadCoursesFromS3(key) {
    return function(callback) {
      console.log('Fetching courses from S3 key: %s', key)
      s3Client.getObject({Bucket: S3_BUCKET, Key: key}, function(err, data) {
        if (!err) {
          callback(null, JSON.parse(data.Body).courses)
        } else {
          console.log('Error fetching courses from S3: %s', err)
          callback(err)
        }
      })
    }
  }

  // Adds a course to the list of launched courses, if `launchedAt` field is set.
  function updateLaunchStatus({launched, unlaunchedUpdated}, course, callback) {
    client.get(util.format(COURSERA_API_ONDEMAND_PATH, course.slug), function(err, res, body) {
      if (!err) {
        if (res.statusCode !== 200) {
          console.log('Unexpected Status Code %s', res.statusCode)
          callback(new Error(util.format('Unexpected Status Code %s', res.statusCode)))
        } else {
          if (body.elements[0].launchedAt) {
            console.log('Adding course: %s', course.slug)
            launched.push(course)
          }
          callback(null, {launched, unlaunchedUpdated})
        }
      } else {
        console.log('Error fetching info for %s: %s', course.slug, err)
        callback(err)
      }
    })
  }

  // Mock function that does nothing. (Used instead of `updateLaunchStatus`)
  function updateLaunchStatusMock(newLaunchedCourses, course, callback) {
    // console.log('Checking course: %s', course.slug)
    callback(null, newLaunchedCourses)
  }

  // Compares course IDs from Coursera with the cached copies and performs
  // updates where necessary. Also iterates through any unlaunched courses
  // and checks if they have now been launched, and adds them to the
  // launched list.
  function updateCache({courseraOnDemand, cachedOnDemand, cachedOnDemandLaunched}, callback) {
    let cIds = _.pluck(courseraOnDemand, 'id')
    let odIds = _.pluck(cachedOnDemand, 'id')

    let newOnDemand = _.map(_.without(cIds, ...odIds), (id) => _.find(courseraOnDemand, {id}))

    console.log('Found %d new courses', newOnDemand.length)

    cachedOnDemand.push(...newOnDemand)
    odIds.push(...(_.pluck(newOnDemand, 'id')))

    let lIds = _.pluck(cachedOnDemandLaunched, 'id')

    let onDemandUnlaunched = _.map(_.without(cIds, ...lIds), (id) => _.find(cachedOnDemand, {id}))

    console.log('Checking launch status for %d courses', onDemandUnlaunched.length)

    async.reduce(
      onDemandUnlaunched,
      {launched: [], unlaunchedUpdated: []},
      event.isMock ? updateLaunchStatusMock : updateLaunchStatus,
      function(err, {launched, unlaunchedUpdated}) {
        if (err) {
          console.log('Error filtering for launched courses: %s', err)
          callback(err)
          return
        }
        console.log('Found %d new launched courses', launched.length)
        cachedOnDemandLaunched.push(...launched)
        _.each(unlaunchedUpdated, (c) => {
          var idx = _.indexOf(cachedOnDemand, _.find(cachedOnDemand, {id: c.id}))

          cachedOnDemand.splice(idx, 1, c)
        })
        callback(null, {cachedOnDemand, cachedOnDemandLaunched, newOnDemand, newLaunched: launched})
      })
  }

  // Test function to save courses to a JSON file.
  function saveUpdatedCoursesToFile({cachedOnDemand, cachedOnDemandLaunched, newOnDemand, newLaunched}, callback) {
    console.log('Saving new data…')
    fs.writeFileSync('fixtures/all_ondemand_new.json', JSON.stringify({courses: cachedOnDemand}, 2, 2))
    fs.writeFileSync('fixtures/launched_new.json', JSON.stringify({courses: cachedOnDemandLaunched}, 2, 2))
    callback(null, {newOnDemand, newLaunched})
  }

  // Update cached lists of On Demand Courses, after making a copy of them.
  function saveUpdatedCoursesToS3({cachedOnDemand, cachedOnDemandLaunched, newOnDemand, newLaunched}, callback) {
    async.series([
      (cb) => s3Client.copyObject({Bucket: S3_BUCKET, CopySource: util.format('%s/%s', S3_BUCKET, CACHED_ONDEMAND_KEY), Key: util.format('%s-%s.json', CACHED_ONDEMAND_KEY.slice(0, -5), moment().format('DD-MM-YYYY-HHMM'))}, cb),
      (cb) => s3Client.copyObject({Bucket: S3_BUCKET, CopySource: util.format('%s/%s', S3_BUCKET, CACHED_ONDEMAND_LAUNCHED_KEY), Key: util.format('%s-%s.json', CACHED_ONDEMAND_LAUNCHED_KEY.slice(0, -5), moment().format('DD-MM-YYYY-HHMM'))}, cb),
      (cb) => s3Client.putObject({Bucket: S3_BUCKET, Key: CACHED_ONDEMAND_KEY, Body: JSON.stringify({courses: cachedOnDemand})}, cb),
      (cb) => s3Client.putObject({Bucket: S3_BUCKET, Key: CACHED_ONDEMAND_LAUNCHED_KEY, Body: JSON.stringify({courses: cachedOnDemandLaunched})}, cb)
    ], function(err, result) {
      if (err) {
        callback(err)
      } else {
        callback(null, {s3Responses: result, newOnDemand, newLaunched})
      }
    })
  }

  // Prepares a notification email to send via SES
  function prepNotificationEmail({newOnDemand, newLaunched}) {
    let newOnDemandTxt = _.map(newOnDemand, (c) => `${c.name}
http://coursera.org/learn/${c.slug}`).join('\n\n')
    let newLaunchedTxt = _.map(newLaunched, (c) => `${c.name}
http://coursera.org/learn/${c.slug}`).join('\n\n')
    let body = ['New On-Demand Courses added:\n',
      newOnDemandTxt || 'None',
      '\n',
      'New On-Demand Courses launched:\n',
      newLaunchedTxt || 'None'
    ].join('\n')

    return {
        Destination: {
          ToAddresses: ['d@moocfetcher.com']
        },
        Message: {
          Body: {
            Text: {
              Data: body,
              Charset: 'utf-8'
            }
          },
          Subject: {
            Data: 'Coursera Update',
            Charset: 'utf-8'
          }
        },
        Source: 'MOOCFetcher <contact@moocfetcher.com>'
      }
  }

  // Send a notification email containing newly added and launched courses
  function sendNotificationEmail({newOnDemand, newLaunched}, cb) {
    if ((newOnDemand.length === 0) && (newLaunched.length === 0)) {
      cb()
      return
    }

    let params = prepNotificationEmail({newOnDemand, newLaunched})

    if (event.isMock) {
      console.log('Email:\n%s', params.Message.Body.Text.Data)
      cb()
    } else {
      sesClient.sendEmail(params, (err) => {
        if (err) {
          console.log(`Error sending e-mail: ${err}`, err.stack)
        }
        cb()
      })
    }
  }

  // Updates AWS Lambda context after checking the final results.
  function updateContext(err, result) {
    if (err) {
      context.fail(err)
    } else {
      sendNotificationEmail(result, () => {
        context.succeed(result)
      })
    }
  }

  // Collect the current course lists and trigger update processing.
  function processCourses(err, [courseraAll, cachedOnDemand, cachedOnDemandLaunched]) {
    let courseraOnDemand = _.filter(courseraAll, (c) => c.courseType === 'v2.ondemand')

    if (err) {
      console.log('Failed due to error: %s', err)
      context.fail(err)
    } else {
      async.waterfall([
        async.apply(updateCache, {courseraOnDemand, cachedOnDemand, cachedOnDemandLaunched}),
        event.isMock ? saveUpdatedCoursesToFile : saveUpdatedCoursesToS3
      ], updateContext)
    }
  }

  // Load courses from S3 and Coursera (or test fixtures, if isMock is set)
  async.parallel(
    event.isMock ? [
      loadCoursesFromFile('fixtures/all.json'),
      loadCoursesFromFile('fixtures/all_ondemand.json'),
      loadCoursesFromFile('fixtures/launched.json')
    ] : [
      loadFromCoursera,
      loadCoursesFromS3(CACHED_ONDEMAND_KEY),
      loadCoursesFromS3(CACHED_ONDEMAND_LAUNCHED_KEY)
    ],
    processCourses)
}
<commit_msg>Adjust to new paged API call for getting all courses. Fix #11<commit_after>/* Lambda function to query the Coursera API and pull the latest OnDemand
 * courses.
 *
 * This function does the following:
 *
 * - Queries the Coursera API to retrieve all On Demand courses
 *
 * - Updates a cached list of On Demand courses on S3. (CACHED_ONDEMAND_KEY)
 *
 * - Queries the Coursera API to check if any previously unlaunched On Demand
 *   courses have been launched, and adds them to the cached list of launched
 *   On Demand courses. (CACHED_ONDEMAND_LAUNCHED_KEY)
 *
 * - Sends a notification email if any new On Demand courses are added or
 *   launched
 *
 * TESTING
 * -------
 * Check the file test-end-to-end.js.
 *
 * We set a special boolean property `isMock` inside the event object passed
 * into the handler, which triggers offline behavior at certain places.
 */

import AWS from 'aws-sdk'
import request from 'request-json'
import _ from 'lodash'
import async from 'async'
import fs from 'fs'
import util from 'util'
import moment from 'moment'

const S3_BUCKET = 'moocfetcher'
const CACHED_ONDEMAND_LAUNCHED_KEY = 'coursera/ondemand/launched.json'
const CACHED_ONDEMAND_KEY = 'coursera/ondemand/all.json'
const COURSERA_API_COURSES_PATH = '/api/courses.v1'
const COURSERA_API_ONDEMAND_PATH = '/api/onDemandCourses.v1?&q=slug&slug=%s'
const client = request.createClient('https://www.coursera.org')
const s3Client = new AWS.S3()
const sesClient = new AWS.SES({region: 'us-east-1'})

exports.handler = function(event, context) {
  // Test function to load courses from a JSON file.
  function loadCoursesFromFile(f) {
    return function(callback) {
      console.log('Fetching courses from file %s…', f)
      callback(null, JSON.parse(fs.readFileSync(f)).courses)
    }
  }

  function loadFromCourseraPaged(callback, next, courses) {
    let url = COURSERA_API_COURSES_PATH

    if (next) {
      url = url + '?start=' + next
    }

    client.get(url, function(err, res, body) {
      if (!err) {
        if (res.statusCode !== 200) {
          console.log('Unexpected Status Code %s', res.statusCode)
          callback(new Error(util.format('Unexpected Status Code %s', res.statusCode)))
        } else {
          let c = courses.concat(body.elements)

          if (body.paging && body.paging.next) {
            loadFromCourseraPaged(callback, body.paging.next, c)
          } else {
            callback(null, courses)
          }
        }
      } else {
        console.log('Error fetching courses from Coursera: %s', err)
        callback(err)
      }
    })
  }

  // Loads all courses by querying the Coursera API.
  function loadFromCoursera(callback) {
    console.log('Fetching courses from Coursera…')
    loadFromCourseraPaged(callback, null, [])
  }

  // Loads a list of cached courses from S3, given the key.
  function loadCoursesFromS3(key) {
    return function(callback) {
      console.log('Fetching courses from S3 key: %s', key)
      s3Client.getObject({Bucket: S3_BUCKET, Key: key}, function(err, data) {
        if (!err) {
          callback(null, JSON.parse(data.Body).courses)
        } else {
          console.log('Error fetching courses from S3: %s', err)
          callback(err)
        }
      })
    }
  }

  // Adds a course to the list of launched courses, if `launchedAt` field is set.
  function updateLaunchStatus({launched, unlaunchedUpdated}, course, callback) {
    client.get(util.format(COURSERA_API_ONDEMAND_PATH, course.slug), function(err, res, body) {
      if (!err) {
        if (res.statusCode !== 200) {
          console.log('Unexpected Status Code %s for course %s', res.statusCode, course.slug)
        } else if (body.elements[0].launchedAt) {
          console.log('Adding course: %s', course.slug)
          launched.push(course)
        }

        callback(null, {launched, unlaunchedUpdated})
      } else {
        console.log('Error fetching info for %s: %s', course.slug, err)
        callback(err)
      }
    })
  }

  // Mock function that does nothing. (Used instead of `updateLaunchStatus`)
  function updateLaunchStatusMock(newLaunchedCourses, course, callback) {
    // console.log('Checking course: %s', course.slug)
    callback(null, newLaunchedCourses)
  }

  // Compares course IDs from Coursera with the cached copies and performs
  // updates where necessary. Also iterates through any unlaunched courses
  // and checks if they have now been launched, and adds them to the
  // launched list.
  function updateCache({courseraOnDemand, cachedOnDemand, cachedOnDemandLaunched}, callback) {
    let cIds = _.pluck(courseraOnDemand, 'id')
    let odIds = _.pluck(cachedOnDemand, 'id')

    let newOnDemand = _.map(_.without(cIds, ...odIds), (id) => _.find(courseraOnDemand, {id}))

    console.log('Found %d new courses', newOnDemand.length)

    cachedOnDemand.push(...newOnDemand)
    odIds.push(...(_.pluck(newOnDemand, 'id')))

    let lIds = _.pluck(cachedOnDemandLaunched, 'id')

    let onDemandUnlaunched = _.map(_.without(cIds, ...lIds), (id) => _.find(cachedOnDemand, {id}))

    console.log('Checking launch status for %d courses', onDemandUnlaunched.length)

    async.reduce(
      onDemandUnlaunched,
      {launched: [], unlaunchedUpdated: []},
      event.isMock ? updateLaunchStatusMock : updateLaunchStatus,
      function(err, {launched, unlaunchedUpdated}) {
        if (err) {
          console.log('Error filtering for launched courses: %s', err)
          callback(err)
          return
        }
        console.log('Found %d new launched courses', launched.length)
        cachedOnDemandLaunched.push(...launched)
        _.each(unlaunchedUpdated, (c) => {
          var idx = _.indexOf(cachedOnDemand, _.find(cachedOnDemand, {id: c.id}))

          cachedOnDemand.splice(idx, 1, c)
        })
        callback(null, {cachedOnDemand, cachedOnDemandLaunched, newOnDemand, newLaunched: launched})
      })
  }

  // Test function to save courses to a JSON file.
  function saveUpdatedCoursesToFile({cachedOnDemand, cachedOnDemandLaunched, newOnDemand, newLaunched}, callback) {
    console.log('Saving new data…')
    fs.writeFileSync('fixtures/all_ondemand_new.json', JSON.stringify({courses: cachedOnDemand}, 2, 2))
    fs.writeFileSync('fixtures/launched_new.json', JSON.stringify({courses: cachedOnDemandLaunched}, 2, 2))
    callback(null, {newOnDemand, newLaunched})
  }

  // Update cached lists of On Demand Courses, after making a copy of them.
  function saveUpdatedCoursesToS3({cachedOnDemand, cachedOnDemandLaunched, newOnDemand, newLaunched}, callback) {
    async.series([
      (cb) => s3Client.copyObject({Bucket: S3_BUCKET, CopySource: util.format('%s/%s', S3_BUCKET, CACHED_ONDEMAND_KEY), Key: util.format('%s-%s.json', CACHED_ONDEMAND_KEY.slice(0, -5), moment().format('DD-MM-YYYY-HHMM'))}, cb),
      (cb) => s3Client.copyObject({Bucket: S3_BUCKET, CopySource: util.format('%s/%s', S3_BUCKET, CACHED_ONDEMAND_LAUNCHED_KEY), Key: util.format('%s-%s.json', CACHED_ONDEMAND_LAUNCHED_KEY.slice(0, -5), moment().format('DD-MM-YYYY-HHMM'))}, cb),
      (cb) => s3Client.putObject({Bucket: S3_BUCKET, Key: CACHED_ONDEMAND_KEY, Body: JSON.stringify({courses: cachedOnDemand})}, cb),
      (cb) => s3Client.putObject({Bucket: S3_BUCKET, Key: CACHED_ONDEMAND_LAUNCHED_KEY, Body: JSON.stringify({courses: cachedOnDemandLaunched})}, cb)
    ], function(err, result) {
      if (err) {
        callback(err)
      } else {
        callback(null, {s3Responses: result, newOnDemand, newLaunched})
      }
    })
  }

  // Prepares a notification email to send via SES
  function prepNotificationEmail({newOnDemand, newLaunched}) {
    let newOnDemandTxt = _.map(newOnDemand, (c) => `${c.name}
http://coursera.org/learn/${c.slug}`).join('\n\n')
    let newLaunchedTxt = _.map(newLaunched, (c) => `${c.name}
http://coursera.org/learn/${c.slug}`).join('\n\n')
    let body = ['New On-Demand Courses added:\n',
      newOnDemandTxt || 'None',
      '\n',
      'New On-Demand Courses launched:\n',
      newLaunchedTxt || 'None'
    ].join('\n')

    return {
        Destination: {
          ToAddresses: ['d@moocfetcher.com']
        },
        Message: {
          Body: {
            Text: {
              Data: body,
              Charset: 'utf-8'
            }
          },
          Subject: {
            Data: 'Coursera Update',
            Charset: 'utf-8'
          }
        },
        Source: 'MOOCFetcher <contact@moocfetcher.com>'
      }
  }

  // Send a notification email containing newly added and launched courses
  function sendNotificationEmail({newOnDemand, newLaunched}, cb) {
    if ((newOnDemand.length === 0) && (newLaunched.length === 0)) {
      cb()
      return
    }

    let params = prepNotificationEmail({newOnDemand, newLaunched})

    if (event.isMock) {
      console.log('Email:\n%s', params.Message.Body.Text.Data)
      cb()
    } else {
      sesClient.sendEmail(params, (err) => {
        if (err) {
          console.log(`Error sending e-mail: ${err}`, err.stack)
        }
        cb()
      })
    }
  }

  // Updates AWS Lambda context after checking the final results.
  function updateContext(err, result) {
    if (err) {
      context.fail(err)
    } else {
      sendNotificationEmail(result, () => {
        context.succeed(result)
      })
    }
  }

  // Collect the current course lists and trigger update processing.
  function processCourses(err, [courseraAll, cachedOnDemand, cachedOnDemandLaunched]) {
    console.log('Found %d courses in all…', courseraAll.length)
    let courseraOnDemand = _.filter(courseraAll, (c) => c.courseType === 'v2.ondemand')

    if (err) {
      console.log('Failed due to error: %s', err)
      context.fail(err)
    } else {
      async.waterfall([
        async.apply(updateCache, {courseraOnDemand, cachedOnDemand, cachedOnDemandLaunched}),
        event.isMock ? saveUpdatedCoursesToFile : saveUpdatedCoursesToS3
      ], updateContext)
    }
  }

  // Load courses from S3 and Coursera (or test fixtures, if isMock is set)
  async.parallel(
    event.isMock ? [
      loadCoursesFromFile('fixtures/all.json'),
      loadCoursesFromFile('fixtures/all_ondemand.json'),
      loadCoursesFromFile('fixtures/launched.json')
    ] : [
      loadFromCoursera,
      loadCoursesFromS3(CACHED_ONDEMAND_KEY),
      loadCoursesFromS3(CACHED_ONDEMAND_LAUNCHED_KEY)
    ],
    processCourses)
}
<|endoftext|>


<commit_before>
//
// Adapter for JS TestNet
// https://github.com/kumar303/jstestnet
//
// Be sure this file is loaded *after* qunit/testrunner.js or whatever else
// you're using

(function() {

    var canPost = false;
    try {
        canPost = !!window.top.postMessage;
    } catch(e){}
    if (!canPost) {
        return;
    }

    function postMsg(data) {
        var msg = '';
        for (var k in data) {
            if (msg.length > 0) {
                msg += '&';
            }
            msg += k + '=' + encodeURI(data[k]);
        }
        window.top.postMessage(msg, '*');
    }

    // QUnit (jQuery)
    // http://docs.jquery.com/QUnit
    if ( typeof QUnit !== "undefined" ) {

        QUnit.begin = function() {
            postMsg({
                action: 'hello',
                user_agent: navigator.userAgent
            });
        };

        QUnit.done = function(failures, total) {
            // // Clean up the HTML (remove any un-needed test markup)
            // $("nothiddendiv").remove();
            // $("loadediframe").remove();
            // $("dl").remove();
            // $("main").remove();
            //
            // // Show any collapsed results
            // $('ol').show();

            postMsg({
                action: 'done',
                failures: failures,
                total: total
            });
        };

        QUnit.log = function(result, message, details) {
            // Strip out html:
            message = message.replace(/<(?:.|\s)*?>/g, '');
            var msg = {
                action: 'log',
                result: result,
                message: message,
                stacktrace: null
            };
            if (details) {
                if (typeof(details.source) !== 'undefined') {
                    msg.stacktrace = details.source;
                }
            }
            postMsg(msg);
        };

        QUnit.moduleStart = function(name) {
            postMsg({
                action: 'set_module',
                name: name
            });
        };

        QUnit.testStart = function(name) {
            postMsg({
                action: 'set_test',
                name: name
            });
        };

        // window.TestSwarm.serialize = function(){
        //  // Clean up the HTML (remove any un-needed test markup)
        //  remove("nothiddendiv");
        //  remove("loadediframe");
        //  remove("dl");
        //  remove("main");
        //
        //  // Show any collapsed results
        //  var ol = document.getElementsByTagName("ol");
        //  for ( var i = 0; i < ol.length; i++ ) {
        //      ol[i].style.display = "block";
        //  }
        //
        //  return trimSerialize();
        // };
    } else {
        throw new Error("Cannot adapt to jstestnet: Unknown test runner");
    }

})();
<commit_msg>Reports window.onerror as a test failure<commit_after>
//
// Adapter for JS TestNet
// https://github.com/kumar303/jstestnet
//
// Be sure this file is loaded *after* qunit/testrunner.js or whatever else
// you're using

(function() {

    var canPost = false;
    try {
        canPost = !!window.top.postMessage;
    } catch(e){}
    if (!canPost) {
        return;
    }

    function postMsg(data) {
        var msg = '';
        for (var k in data) {
            if (msg.length > 0) {
                msg += '&';
            }
            msg += k + '=' + encodeURI(data[k]);
        }
        window.top.postMessage(msg, '*');
    }

    window.onerror = function(errorMsg, url, lineNumber) {
        var msg = {
            action: 'log',
            result: false, // failure
            message: 'Exception: ' + errorMsg + ' at ' + url + ':' + lineNumber,
            // TODO(Kumar) add stacktrace on platforms that support it
            // (like Firefox)
            stacktrace: null
        };
        postMsg(msg);
    };

    // QUnit (jQuery)
    // http://docs.jquery.com/QUnit
    if ( typeof QUnit !== "undefined" ) {

        QUnit.begin = function() {
            postMsg({
                action: 'hello',
                user_agent: navigator.userAgent
            });
        };

        QUnit.done = function(failures, total) {
            // // Clean up the HTML (remove any un-needed test markup)
            // $("nothiddendiv").remove();
            // $("loadediframe").remove();
            // $("dl").remove();
            // $("main").remove();
            //
            // // Show any collapsed results
            // $('ol').show();

            postMsg({
                action: 'done',
                failures: failures,
                total: total
            });
        };

        QUnit.log = function(result, message, details) {
            // Strip out html:
            message = message.replace(/<(?:.|\s)*?>/g, '');
            var msg = {
                action: 'log',
                result: result,
                message: message,
                stacktrace: null
            };
            if (details) {
                if (typeof(details.source) !== 'undefined') {
                    msg.stacktrace = details.source;
                }
            }
            postMsg(msg);
        };

        QUnit.moduleStart = function(name) {
            postMsg({
                action: 'set_module',
                name: name
            });
        };

        QUnit.testStart = function(name) {
            postMsg({
                action: 'set_test',
                name: name
            });
        };

        // window.TestSwarm.serialize = function(){
        //  // Clean up the HTML (remove any un-needed test markup)
        //  remove("nothiddendiv");
        //  remove("loadediframe");
        //  remove("dl");
        //  remove("main");
        //
        //  // Show any collapsed results
        //  var ol = document.getElementsByTagName("ol");
        //  for ( var i = 0; i < ol.length; i++ ) {
        //      ol[i].style.display = "block";
        //  }
        //
        //  return trimSerialize();
        // };
    } else {
        throw new Error("Cannot adapt to jstestnet: Unknown test runner");
    }

})();
<|endoftext|>


<commit_before>// draws cromwell timing diagram for a submission  Cromwell 19

function addDataTableRow(dataTable, callName, callPartName, startDate, endDate) {
    if (startDate <= endDate) {
        dataTable.addRow([callName, callPartName, startDate, endDate]);
    } // else {
      //  console.error("Unable to add '" + callName + "'s entry: '" + callPartName + "' because start-time '" + startDate + "'' is greater than end-time '" + endDate + "'");
    //}
}

function timingDiagram(response, workflowName) {
    var container = document.getElementById('chart_div');
    var chart = new google.visualization.Timeline(container);
    var data = JSON.parse(response);

    var dataTable = new google.visualization.DataTable();
    dataTable.addColumn({ type: 'string', id: 'Position' });
    dataTable.addColumn({ type: 'string', id: 'Name' });
    dataTable.addColumn({ type: 'date', id: 'Start' });
    dataTable.addColumn({ type: 'date', id: 'End' });

    var executionCallsCount = 0;
    //var workflowName = data.workflowName;
    var workflowEnd = null;
    if (data.hasOwnProperty("end")) {
        workflowEnd = new Date(data.end);
    }

    var callsList = data.calls;
    var sortable = [];
    for (var key in callsList) {
        sortable.push([key, callsList[key]]);
    }
    sortable.sort(function(a,b) {
        return new Date(a[1][0].start) - new Date(b[1][0].start);
    });

    for(var sortableIndex in sortable)
    {
        var callName = sortable[sortableIndex][0];
        var callList = sortable[sortableIndex][1];

        for (var callIndex in callList)
        {
            var index = callList[callIndex]["shardIndex"];
            var attempt = (typeof callList[callIndex]["attempt"] != "undefined") ? callList[callIndex]["attempt"] : null;
            var callStatus = callList[callIndex].executionStatus;

            // add the index of the shard if there is one
            var thisCallName = (index == -1 ? callName : callName + "." + index);

            // add the retry number, unless this was a successfuly first attempt (for brevity)
            thisCallName = (callStatus == "Done" && attempt === 1 ? thisCallName :
            	( attempt == null) ? thisCallName : thisCallName + ".retry-" + attempt)

            // Remove the workflow name
            //thisCallName = thisCallName.replace(new RegExp("^" + workflowName + "\\."), "");

            var executionEvents = callList[callIndex].executionEvents;

            var firstEventStart = null;
            var finalEventEnd = null;

            if(callStatus == "Done" || callStatus == "Failed" || callStatus == "Preempted") {
                executionCallsCount++;
                //for (var executionEventIndex in executionEvents) {
                for (executionEventIndex=0, len=executionEvents.length; executionEventIndex < len; executionEventIndex++) {
                    var executionEvent = callList[callIndex].executionEvents[executionEventIndex];
                    var description = executionEvent["description"];
                    // Replace all "start(...)" with just "start" so that the names (and therefore the "color" assigned from the list below) are consistent:
                    //description = description.replace(new RegExp("^start.*"), "start");
                    var startDate = new Date(executionEvent["startTime"]);
                    var endDate = new Date(executionEvent["endTime"]);
                    if (firstEventStart == null || startDate < firstEventStart) {
                        firstEventStart = startDate
                    }
                    if (finalEventEnd == null || endDate > finalEventEnd) {
                        finalEventEnd = endDate
                    }
                    // NB: See the column definitions above.
	                addDataTableRow(dataTable, thisCallName, description, startDate, endDate);
                }

                if (callList[callIndex].hasOwnProperty("start")) {
                    var callStart = new Date(callList[callIndex].start);
                }
                else {
                    var callStart = firstEventStart
                }
                if (callList[callIndex].hasOwnProperty("end")) {
                    var callEnd = new Date(callList[callIndex].end);
                }
                else {
                    var callStart = finalEventEnd
                }

                if (firstEventStart == null || finalEventEnd == null) {
                    addDataTableRow(dataTable, thisCallName, callList[callIndex].executionStatus, callStart, callEnd);
                } else {
                    if (callStart < firstEventStart) addDataTableRow(dataTable, thisCallName, "cromwell starting overhead", callStart, firstEventStart);
                    if (callEnd > finalEventEnd) addDataTableRow(dataTable, thisCallName, "cromwell final overhead", finalEventEnd, callEnd);
                }
            } else if (callList[callIndex].executionStatus == "Running") {
                executionCallsCount++;
                var endDate = workflowEnd;
                if(endDate == null) {
                    addDataTableRow(dataTable, thisCallName, "Running", new Date(callList[callIndex].start), new Date(Date.now()));
                }
                else {
                    addDataTableRow(dataTable, thisCallName, "Still running when workflow ended", new Date(callList[callIndex].start), endDate);
                }
            } else if (callList[callIndex].executionStatus == "Starting") {
                executionCallsCount++;
                var endDate = new Date(Date.now());
                addDataTableRow(dataTable, thisCallName, "Starting", new Date(callList[callIndex].start), endDate);
            }
        }
    }

    var options = {
        colors: ['#04E9E7', '#009DF4', '#0201F4', '#01FC01', '#00C400', '#008C00', '#CCAD51', '#2dd801', '#F99200', '#9854C6', '#F800FD', '#BC0000', '#FD0000'],
        backgroundColor: '#ffffff',
        height: executionCallsCount * 18 + 85,
        timeline: {
            avoidOverlappingGridLines: false,
            showBarLabels: false,
            rowLabelStyle: { fontName: 'Roboto', fontSize: 12, color: '#333' },   //font-size 10   #603913
            // Although bar labels are unshown, they still affect the height of each row. So make it small.
            barLabelStyle: { fontName: 'Roboto', fontSize: 8, color: '#333' },    //#603913
        }
    };
    chart.draw(dataTable, options);
};

// stored to preserve function calls through Closure
window['addDataTableRow'] = addDataTableRow;
window['timingDiagram'] = timingDiagram;
<commit_msg>GAWB-1397: Changing timing diagram height<commit_after>// draws cromwell timing diagram for a submission  Cromwell 19

function addDataTableRow(dataTable, callName, callPartName, startDate, endDate) {
    if (startDate <= endDate) {
        dataTable.addRow([callName, callPartName, startDate, endDate]);
    } // else {
      //  console.error("Unable to add '" + callName + "'s entry: '" + callPartName + "' because start-time '" + startDate + "'' is greater than end-time '" + endDate + "'");
    //}
}

function timingDiagram(response, workflowName) {
    var container = document.getElementById('chart_div');
    var chart = new google.visualization.Timeline(container);
    var data = JSON.parse(response);

    var dataTable = new google.visualization.DataTable();
    dataTable.addColumn({ type: 'string', id: 'Position' });
    dataTable.addColumn({ type: 'string', id: 'Name' });
    dataTable.addColumn({ type: 'date', id: 'Start' });
    dataTable.addColumn({ type: 'date', id: 'End' });

    var executionCallsCount = 0;
    //var workflowName = data.workflowName;
    var workflowEnd = null;
    if (data.hasOwnProperty("end")) {
        workflowEnd = new Date(data.end);
    }

    var callsList = data.calls;
    var sortable = [];
    for (var key in callsList) {
        sortable.push([key, callsList[key]]);
    }
    sortable.sort(function(a,b) {
        return new Date(a[1][0].start) - new Date(b[1][0].start);
    });

    for(var sortableIndex in sortable)
    {
        var callName = sortable[sortableIndex][0];
        var callList = sortable[sortableIndex][1];

        for (var callIndex in callList)
        {
            var index = callList[callIndex]["shardIndex"];
            var attempt = (typeof callList[callIndex]["attempt"] != "undefined") ? callList[callIndex]["attempt"] : null;
            var callStatus = callList[callIndex].executionStatus;

            // add the index of the shard if there is one
            var thisCallName = (index == -1 ? callName : callName + "." + index);

            // add the retry number, unless this was a successfuly first attempt (for brevity)
            thisCallName = (callStatus == "Done" && attempt === 1 ? thisCallName :
            	( attempt == null) ? thisCallName : thisCallName + ".retry-" + attempt)

            // Remove the workflow name
            //thisCallName = thisCallName.replace(new RegExp("^" + workflowName + "\\."), "");

            var executionEvents = callList[callIndex].executionEvents;

            var firstEventStart = null;
            var finalEventEnd = null;

            if(callStatus == "Done" || callStatus == "Failed" || callStatus == "Preempted") {
                executionCallsCount++;
                //for (var executionEventIndex in executionEvents) {
                for (executionEventIndex=0, len=executionEvents.length; executionEventIndex < len; executionEventIndex++) {
                    var executionEvent = callList[callIndex].executionEvents[executionEventIndex];
                    var description = executionEvent["description"];
                    // Replace all "start(...)" with just "start" so that the names (and therefore the "color" assigned from the list below) are consistent:
                    //description = description.replace(new RegExp("^start.*"), "start");
                    var startDate = new Date(executionEvent["startTime"]);
                    var endDate = new Date(executionEvent["endTime"]);
                    if (firstEventStart == null || startDate < firstEventStart) {
                        firstEventStart = startDate
                    }
                    if (finalEventEnd == null || endDate > finalEventEnd) {
                        finalEventEnd = endDate
                    }
                    // NB: See the column definitions above.
	                addDataTableRow(dataTable, thisCallName, description, startDate, endDate);
                }

                if (callList[callIndex].hasOwnProperty("start")) {
                    var callStart = new Date(callList[callIndex].start);
                }
                else {
                    var callStart = firstEventStart
                }
                if (callList[callIndex].hasOwnProperty("end")) {
                    var callEnd = new Date(callList[callIndex].end);
                }
                else {
                    var callStart = finalEventEnd
                }

                if (firstEventStart == null || finalEventEnd == null) {
                    addDataTableRow(dataTable, thisCallName, callList[callIndex].executionStatus, callStart, callEnd);
                } else {
                    if (callStart < firstEventStart) addDataTableRow(dataTable, thisCallName, "cromwell starting overhead", callStart, firstEventStart);
                    if (callEnd > finalEventEnd) addDataTableRow(dataTable, thisCallName, "cromwell final overhead", finalEventEnd, callEnd);
                }
            } else if (callList[callIndex].executionStatus == "Running") {
                executionCallsCount++;
                var endDate = workflowEnd;
                if(endDate == null) {
                    addDataTableRow(dataTable, thisCallName, "Running", new Date(callList[callIndex].start), new Date(Date.now()));
                }
                else {
                    addDataTableRow(dataTable, thisCallName, "Still running when workflow ended", new Date(callList[callIndex].start), endDate);
                }
            } else if (callList[callIndex].executionStatus == "Starting") {
                executionCallsCount++;
                var endDate = new Date(Date.now());
                addDataTableRow(dataTable, thisCallName, "Starting", new Date(callList[callIndex].start), endDate);
            }
        }
    }

    var options = {
        colors: ['#04E9E7', '#009DF4', '#0201F4', '#01FC01', '#00C400', '#008C00', '#CCAD51', '#2dd801', '#F99200', '#9854C6', '#F800FD', '#BC0000', '#FD0000'],
        backgroundColor: '#ffffff',
        height: (executionCallsCount + 2) * 27.328,
        timeline: {
            avoidOverlappingGridLines: false,
            showBarLabels: false,
            rowLabelStyle: { fontName: 'Roboto', fontSize: 12, color: '#333' },   //font-size 10   #603913
            // Although bar labels are unshown, they still affect the height of each row. So make it small.
            barLabelStyle: { fontName: 'Roboto', fontSize: 8, color: '#333' },    //#603913
        }
    };
    chart.draw(dataTable, options);
};

// stored to preserve function calls through Closure
window['addDataTableRow'] = addDataTableRow;
window['timingDiagram'] = timingDiagram;
<|endoftext|>


<commit_before>function precise(x) {
  return Number.parseFloat(x).toPrecision(4);
}

console.log(precise(123.456));
// expected output: "123.5"

console.log(precise(0.004));
// expected output: "0.004000"

console.log(precise('1.23e+5'));
// expected output: "1.230e+5"
<commit_msg>Remove use of Number.parseFloat in toPrecision example (#2001)<commit_after>function precise(x) {
  return x.toPrecision(4);
}

console.log(precise(123.456));
// expected output: "123.5"

console.log(precise(0.004));
// expected output: "0.004000"

console.log(precise(1.23e5));
// expected output: "1.230e+5"
<|endoftext|>