<commit_before>import _ from 'lodash';
import moment from 'moment';
import { Observable } from 'rx';
import assign from 'object.assign';
import debugFactory from 'debug';
import utils from '../utils';

import {
  saveUser,
  observeMethod,
  observableQueryFromModel
} from '../utils/rx';

import {
  userMigration,
  ifNoUserRedirectTo,
  ifNoUserSend
} from '../utils/middleware';

const debug = debugFactory('freecc:challenges');
const challengeMapWithNames = utils.getChallengeMapWithNames();
const challengeMapWithIds = utils.getChallengeMapWithIds();
const challengeMapWithDashedNames = utils.getChallengeMapWithDashedNames();
const challangesRegex = /^(bonfire|waypoint|zipline|basejump)/i;
const firstChallenge = 'waypoint-say-hello-to-html-elements';

const dasherize = utils.dasherize;
const unDasherize = utils.unDasherize;
const getMDNLinks = utils.getMDNLinks;

function numberWithCommas(x) {
  return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

function updateUserProgress(user, challengeId, completedChallenge) {
  const alreadyCompleted = user.completedChallenges.some(({ id }) => {
    return id === challengeId;
  });

  if (!alreadyCompleted) {
    user.progressTimestamps.push({
      timestamp: Date.now(),
      completedChallenge
    });
  }
  user.completedChallenges.push(completedChallenge);
  return user;
}

module.exports = function(app) {
  const router = app.loopback.Router();

  const challengesQuery = {
    order: [
      'order ASC',
      'suborder ASC'
    ]
  };

  // challenge model
  const Challenge = app.models.Challenge;
  // challenge find query stream
  const findChallenge$ = observeMethod(Challenge, 'find');
  // create a stream of all the challenges
  const challenge$ = findChallenge$(challengesQuery)
    .flatMap(challenges => Observable.from(challenges))
    .shareReplay();

  // create a stream of challenge blocks
  const blocks$ = challenge$
    // group challenges by block | returns a stream of observables
    .groupBy(challenge => challenge.block)
    // turn block group stream into an array
    .flatMap(block$ => block$.toArray())
    // turn array into stream of object
    .map(blockArray => ({
      name: blockArray[0].block,
      dashedName: dasherize(blockArray[0].block),
      challenges: blockArray
    }))
    .filter(({ name })=> {
      return name !== 'Hikes';
    })
    .shareReplay();

  const User = app.models.User;
  const userCount$ = observeMethod(User, 'count');

  const send200toNonUser = ifNoUserSend(true);
  const redirectNonUser = ifNoUserRedirectTo(
    '/challenges/learn-how-free-code-camp-works'
  );

  router.post(
    '/completed-challenge/',
    send200toNonUser,
    completedChallenge
  );
  router.post(
    '/completed-zipline-or-basejump',
    send200toNonUser,
    completedZiplineOrBasejump
  );
  router.post(
    '/completed-bonfire',
    send200toNonUser,
    completedBonfire
  );

  // the follow routes are covered by userMigration
  router.use(userMigration);
  router.get('/map', challengeMap);
  router.get(
    '/challenges/next-challenge',
    redirectNonUser,
    returnNextChallenge
  );

  router.get('/challenges/:challengeName', returnIndividualChallenge);

  router.get(
    '/challenges/',
    redirectNonUser,
    returnCurrentChallenge
  );

  app.use(router);

  function returnNextChallenge(req, res, next) {

    // find the user's current challenge and block
    // look in that block and find the index of their current challenge
    // if index + 1 < block.challenges.length
    // serve index + 1 challenge
    // otherwise increment block key and serve the first challenge in that block
    // unless the next block is undefined, which means no next block
    let nextChallengeName = firstChallenge;

    const challengeId = req.user.currentChallenge.challengeId;
    // find challenge
    return challenge$
      .filter(({ block }) => block !== 'Hikes')
      .filter(({ id }) => id === challengeId)
      // now lets find the block it belongs to
      .flatMap(challenge => {
        // find the index of the block this challenge resides in
        const blockIndex$ = blocks$
          .findIndex(({ name }) => name === challenge.block);

        return blockIndex$
          .flatMap(blockIndex => {
            // could not find block?
            if (blockIndex === -1) {
              return Observable.throw(
                'could not find challenge block for ' + challenge.block
              );
            }
            const nextBlock$ = blocks$.elementAt(blockIndex + 1);
            const firstChallengeOfNextBlock$ = nextBlock$
              .map(block => block.challenges[0]);

            return blocks$
              .elementAt(blockIndex)
              .flatMap(block => {
                // find where our challenge lies in the block
                const challengeIndex$ = Observable.from(block.challenges)
                  .findIndex(({ id }) => id === challengeId);

                // grab next challenge in this block
                return challengeIndex$
                  .map(index => {
                    return block.challenges[index + 1];
                  })
                  .flatMap(nextChallenge => {
                    if (!nextChallenge) {
                      return firstChallengeOfNextBlock$;
                    }
                    return Observable.just(nextChallenge);
                  });
              });
          });
      })
      .map(nextChallenge => {
        nextChallengeName = nextChallenge.dashedName;
        return nextChallengeName;
      })
      .flatMap(() => {
        return saveUser(req.user);
      })
      .subscribe(
        function() {},
        next,
        function() {
          debug('next challengeName', nextChallengeName);
          if (!nextChallengeName || nextChallengeName === firstChallenge) {
            req.flash('errors', {
              msg: 'It looks like you have finished all of our challenges.' +
              ' Great job! Now on to helping nonprofits!'
            });
            return res.redirect('/challenges/' + firstChallenge);
          }
          res.redirect('/challenges/' + nextChallengeName);
        }
      );

  }

  function returnCurrentChallenge(req, res, next) {
    if (!req.user.currentChallenge) {
      req.user.currentChallenge = {};
      req.user.currentChallenge.challengeId = challengeMapWithIds['0'][0];
      req.user.currentChallenge.challengeName = challengeMapWithNames['0'][0];
      req.user.currentChallenge.dashedName =
        challengeMapWithDashedNames['0'][0];
    }

    var nameString = req.user.currentChallenge.dashedName;

    saveUser(req.user)
      .subscribe(
        function() {},
        next,
        function() {
          res.redirect('/challenges/' + nameString);
        }
      );
  }

  function returnIndividualChallenge(req, res, next) {
    var origChallengeName = req.params.challengeName;
    var unDashedName = unDasherize(origChallengeName);

    var challengeName = challangesRegex.test(unDashedName) ?
      // remove first word if matches
      unDashedName.split(' ').slice(1).join(' ') :
      unDashedName;

    debug('looking for ', challengeName);
    Challenge.findOne(
      { where: { name: { like: challengeName, options: 'i' } } },
      function(err, challenge) {
        if (err) { return next(err); }

        // Handle not found
        if (!challenge) {
          debug('did not find challenge for ' + origChallengeName);
          req.flash('errors', {
            msg:
              '404: We couldn\'t find a challenge with the name `' +
              origChallengeName +
              '` Please double check the name.'
          });
          return res.redirect('/challenges');
        }
        // Redirect to full name if the user only entered a partial
        if (dasherize(challenge.name) !== origChallengeName) {
          debug('redirecting to fullname');
          return res.redirect('/challenges/' + dasherize(challenge.name));
        }

        if (req.user) {
          req.user.currentChallenge = {
            challengeId: challenge.id,
            challengeName: challenge.name,
            dashedName: challenge.dashedName
          };
        }

        var commonLocals = {
          title: challenge.name,
          dashedName: origChallengeName,
          name: challenge.name,
          details: challenge.description,
          tests: challenge.tests,
          challengeSeed: challenge.challengeSeed,
          verb: utils.randomVerb(),
          phrase: utils.randomPhrase(),
          compliment: utils.randomCompliment(),
          challengeId: challenge.id,
          challengeType: challenge.challengeType,
          // video challenges
          video: challenge.challengeSeed[0],
          // bonfires specific
          difficulty: Math.floor(+challenge.difficulty),
          bonfires: challenge,
          MDNkeys: challenge.MDNlinks,
          MDNlinks: getMDNLinks(challenge.MDNlinks),
          // htmls specific
          environment: utils.whichEnvironment()
        };

        // TODO Berkeley
        var challengeView = {
          0: 'coursewares/showHTML',
          1: 'coursewares/showJS',
          2: 'coursewares/showVideo',
          3: 'coursewares/showZiplineOrBasejump',
          4: 'coursewares/showZiplineOrBasejump',
          5: 'coursewares/showBonfire'
        };

        saveUser(req.user)
          .subscribe(
            function() {},
            next,
            function() {
              var view = challengeView[challenge.challengeType];
              res.render(view, commonLocals);
            }
          );
      });
  }

  function completedBonfire(req, res, next) {
    debug('compltedBonfire');
    var completedWith = req.body.challengeInfo.completedWith || false;
    var challengeId = req.body.challengeInfo.challengeId;

    var challengeData = {
      id: challengeId,
      name: req.body.challengeInfo.challengeName,
      completedDate: Math.round(+new Date()),
      solution: req.body.challengeInfo.solution,
      challengeType: 5
    };

    observableQueryFromModel(
        User,
        'findOne',
        { where: { username: ('' + completedWith).toLowerCase() } }
      )
      .doOnNext(function(pairedWith) {
        debug('paired with ', pairedWith);
        if (pairedWith) {
          updateUserProgress(
            pairedWith,
            challengeId,
            assign({ completedWith: req.user.id }, challengeData)
          );
        }
      })
      .withLatestFrom(
        Observable.just(req.user),
        function(pairedWith, user) {
          return {
            user: user,
            pairedWith: pairedWith
          };
        }
      )
      // side effects should always be done in do's and taps
      .doOnNext(function(dats) {
        updateUserProgress(
          dats.user,
          challengeId,
          dats.pairedWith ?
            // paired programmer found and adding to data
            assign({ completedWith: dats.pairedWith.id }, challengeData) :
            // user said they paired, but pair wasn't found
            challengeData
        );
      })
      // not iterate users
      .flatMap(function(dats) {
        debug('flatmap');
        return Observable.from([dats.user, dats.pairedWith]);
      })
      // save user
      .flatMap(function(user) {
        // save user will do nothing if user is falsey
        return saveUser(user);
      })
      .subscribe(
        function(user) {
          debug('onNext');
          if (user) {
            debug('user %s saved', user.username);
          }
        },
        next,
        function() {
          debug('completed');
          return res.status(200).send(true);
        }
      );
  }

  function completedChallenge(req, res, next) {

    const completedDate = Math.round(+new Date());
    const { id, name } = req.body;
    const { challengeId, challengeName } = req.body.challengeInfo || {};

    updateUserProgress(
      req.user,
      id || challengeId,
      {
        id: id || challengeId,
        completedDate: completedDate,
        name: name || challengeName,
        solution: null,
        githubLink: null,
        verified: true
      }
    );

    saveUser(req.user)
      .subscribe(
        function(user) {
          debug(
            'user save points %s',
            user && user.progressTimestamps && user.progressTimestamps.length
          );
        },
        next,
        function() {
          res.sendStatus(200);
        }
      );
  }

  function completedZiplineOrBasejump(req, res, next) {

    var completedWith = req.body.challengeInfo.completedWith || '';
    var completedDate = Math.round(+new Date());
    var challengeId = req.body.challengeInfo.challengeId;
    var solutionLink = req.body.challengeInfo.publicURL;

    var githubLink = req.body.challengeInfo.challengeType === '4' ?
      req.body.challengeInfo.githubURL :
      true;

    var challengeType = req.body.challengeInfo.challengeType === '4' ?
      4 :
      3;

    if (!solutionLink || !githubLink) {
      req.flash('errors', {
        msg: 'You haven\'t supplied the necessary URLs for us to inspect ' +
        'your work.'
      });
      return res.sendStatus(403);
    }

    var challengeData = {
      id: challengeId,
      name: req.body.challengeInfo.challengeName,
      completedDate: completedDate,
      solution: solutionLink,
      githubLink: githubLink,
      challengeType: challengeType,
      verified: false
    };

    observableQueryFromModel(
        User,
        'findOne',
        { where: { username: completedWith.toLowerCase() } }
      )
      .doOnNext(function(pairedWith) {
        if (pairedWith) {
          updateUserProgress(
            pairedWith,
            challengeId,
            assign({ completedWith: req.user.id }, challengeData)
          );
        }
      })
      .withLatestFrom(Observable.just(req.user), function(pairedWith, user) {
        return {
          user: user,
          pairedWith: pairedWith
        };
      })
      .doOnNext(function({ user, pairedWith }) {
        updateUserProgress(
          user,
          challengeId,
          pairedWith ?
            assign({ completedWith: pairedWith.id }, challengeData) :
            challengeData
        );
      })
      .flatMap(function({ user, pairedWith }) {
        return Observable.from([user, pairedWith]);
      })
      // save users
      .flatMap(function(user) {
        // save user will do nothing if user is falsey
        return saveUser(user);
      })
      .subscribe(
        function(user) {
          if (user) {
            debug('user %s saved', user.username);
          }
        },
        next,
        function() {
          return res.status(200).send(true);
        }
      );
  }

  function challengeMap({ user = {} }, res, next) {
    const daysRunning = moment().diff(new Date('10/15/2014'), 'days');

    // if user
    // get the id's of all the users completed challenges
    const completedChallenges = !user.completedChallenges ?
      [] :
      _.uniq(user.completedChallenges).map(({ id }) => id);

    const camperCount$ = userCount$()
      .map(camperCount => numberWithCommas(camperCount));

    // create a stream of an array of all the challenge blocks
    const blocks$ = challenge$
      // mark challenge completed
      .map(challenge => {
        if (completedChallenges.indexOf(challenge.id) !== -1) {
          challenge.completed = true;
        }
        return challenge;
      })
      // group challenges by block | returns a stream of observables
      .groupBy(challenge => challenge.block)
      // turn block group stream into an array
      .flatMap(block$ => block$.toArray())
      .map(blockArray => {
        const completedCount = blockArray.reduce((sum, { completed }) => {
          if (completed) {
            return sum + 1;
          }
          return sum;
        }, 0);

        return {
          name: blockArray[0].block,
          dashedName: dasherize(blockArray[0].block),
          challenges: blockArray,
          completed: completedCount / blockArray.length * 100
        };
      })
      .filter(({ name }) => name !== 'Hikes')
      // turn stream of blocks into a stream of an array
      .toArray();

    Observable.combineLatest(
      camperCount$,
      blocks$,
      (camperCount, blocks) => ({ camperCount, blocks })
    )
      .subscribe(
        ({ camperCount, blocks }) => {
          res.render('challengeMap/show', {
            blocks,
            daysRunning,
            camperCount,
            title: "A map of all Free Code Camp's Challenges"
          });
        },
        next
      );
  }
};
<commit_msg>fix should check completed challenge old _id<commit_after>import _ from 'lodash';
import moment from 'moment';
import { Observable } from 'rx';
import assign from 'object.assign';
import debugFactory from 'debug';
import utils from '../utils';

import {
  saveUser,
  observeMethod,
  observableQueryFromModel
} from '../utils/rx';

import {
  userMigration,
  ifNoUserRedirectTo,
  ifNoUserSend
} from '../utils/middleware';

const debug = debugFactory('freecc:challenges');
const challengeMapWithNames = utils.getChallengeMapWithNames();
const challengeMapWithIds = utils.getChallengeMapWithIds();
const challengeMapWithDashedNames = utils.getChallengeMapWithDashedNames();
const challangesRegex = /^(bonfire|waypoint|zipline|basejump)/i;
const firstChallenge = 'waypoint-say-hello-to-html-elements';

const dasherize = utils.dasherize;
const unDasherize = utils.unDasherize;
const getMDNLinks = utils.getMDNLinks;

function numberWithCommas(x) {
  return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

function updateUserProgress(user, challengeId, completedChallenge) {
  const alreadyCompleted = user.completedChallenges.some(({ id }) => {
    return id === challengeId;
  });

  if (!alreadyCompleted) {
    user.progressTimestamps.push({
      timestamp: Date.now(),
      completedChallenge
    });
  }
  user.completedChallenges.push(completedChallenge);
  return user;
}

module.exports = function(app) {
  const router = app.loopback.Router();

  const challengesQuery = {
    order: [
      'order ASC',
      'suborder ASC'
    ]
  };

  // challenge model
  const Challenge = app.models.Challenge;
  // challenge find query stream
  const findChallenge$ = observeMethod(Challenge, 'find');
  // create a stream of all the challenges
  const challenge$ = findChallenge$(challengesQuery)
    .flatMap(challenges => Observable.from(challenges))
    .shareReplay();

  // create a stream of challenge blocks
  const blocks$ = challenge$
    // group challenges by block | returns a stream of observables
    .groupBy(challenge => challenge.block)
    // turn block group stream into an array
    .flatMap(block$ => block$.toArray())
    // turn array into stream of object
    .map(blockArray => ({
      name: blockArray[0].block,
      dashedName: dasherize(blockArray[0].block),
      challenges: blockArray
    }))
    .filter(({ name })=> {
      return name !== 'Hikes';
    })
    .shareReplay();

  const User = app.models.User;
  const userCount$ = observeMethod(User, 'count');

  const send200toNonUser = ifNoUserSend(true);
  const redirectNonUser = ifNoUserRedirectTo(
    '/challenges/learn-how-free-code-camp-works'
  );

  router.post(
    '/completed-challenge/',
    send200toNonUser,
    completedChallenge
  );
  router.post(
    '/completed-zipline-or-basejump',
    send200toNonUser,
    completedZiplineOrBasejump
  );
  router.post(
    '/completed-bonfire',
    send200toNonUser,
    completedBonfire
  );

  // the follow routes are covered by userMigration
  router.use(userMigration);
  router.get('/map', challengeMap);
  router.get(
    '/challenges/next-challenge',
    redirectNonUser,
    returnNextChallenge
  );

  router.get('/challenges/:challengeName', returnIndividualChallenge);

  router.get(
    '/challenges/',
    redirectNonUser,
    returnCurrentChallenge
  );

  app.use(router);

  function returnNextChallenge(req, res, next) {

    // find the user's current challenge and block
    // look in that block and find the index of their current challenge
    // if index + 1 < block.challenges.length
    // serve index + 1 challenge
    // otherwise increment block key and serve the first challenge in that block
    // unless the next block is undefined, which means no next block
    let nextChallengeName = firstChallenge;

    const challengeId = req.user.currentChallenge.challengeId;
    // find challenge
    return challenge$
      .filter(({ block }) => block !== 'Hikes')
      .filter(({ id }) => id === challengeId)
      // now lets find the block it belongs to
      .flatMap(challenge => {
        // find the index of the block this challenge resides in
        const blockIndex$ = blocks$
          .findIndex(({ name }) => name === challenge.block);

        return blockIndex$
          .flatMap(blockIndex => {
            // could not find block?
            if (blockIndex === -1) {
              return Observable.throw(
                'could not find challenge block for ' + challenge.block
              );
            }
            const nextBlock$ = blocks$.elementAt(blockIndex + 1);
            const firstChallengeOfNextBlock$ = nextBlock$
              .map(block => block.challenges[0]);

            return blocks$
              .elementAt(blockIndex)
              .flatMap(block => {
                // find where our challenge lies in the block
                const challengeIndex$ = Observable.from(block.challenges)
                  .findIndex(({ id }) => id === challengeId);

                // grab next challenge in this block
                return challengeIndex$
                  .map(index => {
                    return block.challenges[index + 1];
                  })
                  .flatMap(nextChallenge => {
                    if (!nextChallenge) {
                      return firstChallengeOfNextBlock$;
                    }
                    return Observable.just(nextChallenge);
                  });
              });
          });
      })
      .map(nextChallenge => {
        nextChallengeName = nextChallenge.dashedName;
        return nextChallengeName;
      })
      .flatMap(() => {
        return saveUser(req.user);
      })
      .subscribe(
        function() {},
        next,
        function() {
          debug('next challengeName', nextChallengeName);
          if (!nextChallengeName || nextChallengeName === firstChallenge) {
            req.flash('errors', {
              msg: 'It looks like you have finished all of our challenges.' +
              ' Great job! Now on to helping nonprofits!'
            });
            return res.redirect('/challenges/' + firstChallenge);
          }
          res.redirect('/challenges/' + nextChallengeName);
        }
      );

  }

  function returnCurrentChallenge(req, res, next) {
    if (!req.user.currentChallenge) {
      req.user.currentChallenge = {};
      req.user.currentChallenge.challengeId = challengeMapWithIds['0'][0];
      req.user.currentChallenge.challengeName = challengeMapWithNames['0'][0];
      req.user.currentChallenge.dashedName =
        challengeMapWithDashedNames['0'][0];
    }

    var nameString = req.user.currentChallenge.dashedName;

    saveUser(req.user)
      .subscribe(
        function() {},
        next,
        function() {
          res.redirect('/challenges/' + nameString);
        }
      );
  }

  function returnIndividualChallenge(req, res, next) {
    var origChallengeName = req.params.challengeName;
    var unDashedName = unDasherize(origChallengeName);

    var challengeName = challangesRegex.test(unDashedName) ?
      // remove first word if matches
      unDashedName.split(' ').slice(1).join(' ') :
      unDashedName;

    debug('looking for ', challengeName);
    Challenge.findOne(
      { where: { name: { like: challengeName, options: 'i' } } },
      function(err, challenge) {
        if (err) { return next(err); }

        // Handle not found
        if (!challenge) {
          debug('did not find challenge for ' + origChallengeName);
          req.flash('errors', {
            msg:
              '404: We couldn\'t find a challenge with the name `' +
              origChallengeName +
              '` Please double check the name.'
          });
          return res.redirect('/challenges');
        }
        // Redirect to full name if the user only entered a partial
        if (dasherize(challenge.name) !== origChallengeName) {
          debug('redirecting to fullname');
          return res.redirect('/challenges/' + dasherize(challenge.name));
        }

        if (req.user) {
          req.user.currentChallenge = {
            challengeId: challenge.id,
            challengeName: challenge.name,
            dashedName: challenge.dashedName
          };
        }

        var commonLocals = {
          title: challenge.name,
          dashedName: origChallengeName,
          name: challenge.name,
          details: challenge.description,
          tests: challenge.tests,
          challengeSeed: challenge.challengeSeed,
          verb: utils.randomVerb(),
          phrase: utils.randomPhrase(),
          compliment: utils.randomCompliment(),
          challengeId: challenge.id,
          challengeType: challenge.challengeType,
          // video challenges
          video: challenge.challengeSeed[0],
          // bonfires specific
          difficulty: Math.floor(+challenge.difficulty),
          bonfires: challenge,
          MDNkeys: challenge.MDNlinks,
          MDNlinks: getMDNLinks(challenge.MDNlinks),
          // htmls specific
          environment: utils.whichEnvironment()
        };

        // TODO Berkeley
        var challengeView = {
          0: 'coursewares/showHTML',
          1: 'coursewares/showJS',
          2: 'coursewares/showVideo',
          3: 'coursewares/showZiplineOrBasejump',
          4: 'coursewares/showZiplineOrBasejump',
          5: 'coursewares/showBonfire'
        };

        saveUser(req.user)
          .subscribe(
            function() {},
            next,
            function() {
              var view = challengeView[challenge.challengeType];
              res.render(view, commonLocals);
            }
          );
      });
  }

  function completedBonfire(req, res, next) {
    debug('compltedBonfire');
    var completedWith = req.body.challengeInfo.completedWith || false;
    var challengeId = req.body.challengeInfo.challengeId;

    var challengeData = {
      id: challengeId,
      name: req.body.challengeInfo.challengeName,
      completedDate: Math.round(+new Date()),
      solution: req.body.challengeInfo.solution,
      challengeType: 5
    };

    observableQueryFromModel(
        User,
        'findOne',
        { where: { username: ('' + completedWith).toLowerCase() } }
      )
      .doOnNext(function(pairedWith) {
        debug('paired with ', pairedWith);
        if (pairedWith) {
          updateUserProgress(
            pairedWith,
            challengeId,
            assign({ completedWith: req.user.id }, challengeData)
          );
        }
      })
      .withLatestFrom(
        Observable.just(req.user),
        function(pairedWith, user) {
          return {
            user: user,
            pairedWith: pairedWith
          };
        }
      )
      // side effects should always be done in do's and taps
      .doOnNext(function(dats) {
        updateUserProgress(
          dats.user,
          challengeId,
          dats.pairedWith ?
            // paired programmer found and adding to data
            assign({ completedWith: dats.pairedWith.id }, challengeData) :
            // user said they paired, but pair wasn't found
            challengeData
        );
      })
      // not iterate users
      .flatMap(function(dats) {
        debug('flatmap');
        return Observable.from([dats.user, dats.pairedWith]);
      })
      // save user
      .flatMap(function(user) {
        // save user will do nothing if user is falsey
        return saveUser(user);
      })
      .subscribe(
        function(user) {
          debug('onNext');
          if (user) {
            debug('user %s saved', user.username);
          }
        },
        next,
        function() {
          debug('completed');
          return res.status(200).send(true);
        }
      );
  }

  function completedChallenge(req, res, next) {

    const completedDate = Math.round(+new Date());
    const { id, name } = req.body;
    const { challengeId, challengeName } = req.body.challengeInfo || {};

    updateUserProgress(
      req.user,
      id || challengeId,
      {
        id: id || challengeId,
        completedDate: completedDate,
        name: name || challengeName,
        solution: null,
        githubLink: null,
        verified: true
      }
    );

    saveUser(req.user)
      .subscribe(
        function(user) {
          debug(
            'user save points %s',
            user && user.progressTimestamps && user.progressTimestamps.length
          );
        },
        next,
        function() {
          res.sendStatus(200);
        }
      );
  }

  function completedZiplineOrBasejump(req, res, next) {

    var completedWith = req.body.challengeInfo.completedWith || '';
    var completedDate = Math.round(+new Date());
    var challengeId = req.body.challengeInfo.challengeId;
    var solutionLink = req.body.challengeInfo.publicURL;

    var githubLink = req.body.challengeInfo.challengeType === '4' ?
      req.body.challengeInfo.githubURL :
      true;

    var challengeType = req.body.challengeInfo.challengeType === '4' ?
      4 :
      3;

    if (!solutionLink || !githubLink) {
      req.flash('errors', {
        msg: 'You haven\'t supplied the necessary URLs for us to inspect ' +
        'your work.'
      });
      return res.sendStatus(403);
    }

    var challengeData = {
      id: challengeId,
      name: req.body.challengeInfo.challengeName,
      completedDate: completedDate,
      solution: solutionLink,
      githubLink: githubLink,
      challengeType: challengeType,
      verified: false
    };

    observableQueryFromModel(
        User,
        'findOne',
        { where: { username: completedWith.toLowerCase() } }
      )
      .doOnNext(function(pairedWith) {
        if (pairedWith) {
          updateUserProgress(
            pairedWith,
            challengeId,
            assign({ completedWith: req.user.id }, challengeData)
          );
        }
      })
      .withLatestFrom(Observable.just(req.user), function(pairedWith, user) {
        return {
          user: user,
          pairedWith: pairedWith
        };
      })
      .doOnNext(function({ user, pairedWith }) {
        updateUserProgress(
          user,
          challengeId,
          pairedWith ?
            assign({ completedWith: pairedWith.id }, challengeData) :
            challengeData
        );
      })
      .flatMap(function({ user, pairedWith }) {
        return Observable.from([user, pairedWith]);
      })
      // save users
      .flatMap(function(user) {
        // save user will do nothing if user is falsey
        return saveUser(user);
      })
      .subscribe(
        function(user) {
          if (user) {
            debug('user %s saved', user.username);
          }
        },
        next,
        function() {
          return res.status(200).send(true);
        }
      );
  }

  function challengeMap({ user = {} }, res, next) {
    const daysRunning = moment().diff(new Date('10/15/2014'), 'days');

    // if user
    // get the id's of all the users completed challenges
    const completedChallenges = !user.completedChallenges ?
      [] :
      _.uniq(user.completedChallenges).map(({ id, _id }) => id || _id);

    const camperCount$ = userCount$()
      .map(camperCount => numberWithCommas(camperCount));

    // create a stream of an array of all the challenge blocks
    const blocks$ = challenge$
      // mark challenge completed
      .map(challenge => {
        if (completedChallenges.indexOf(challenge.id) !== -1) {
          challenge.completed = true;
        }
        return challenge;
      })
      // group challenges by block | returns a stream of observables
      .groupBy(challenge => challenge.block)
      // turn block group stream into an array
      .flatMap(block$ => block$.toArray())
      .map(blockArray => {
        const completedCount = blockArray.reduce((sum, { completed }) => {
          if (completed) {
            return sum + 1;
          }
          return sum;
        }, 0);

        return {
          name: blockArray[0].block,
          dashedName: dasherize(blockArray[0].block),
          challenges: blockArray,
          completed: completedCount / blockArray.length * 100
        };
      })
      .filter(({ name }) => name !== 'Hikes')
      // turn stream of blocks into a stream of an array
      .toArray();

    Observable.combineLatest(
      camperCount$,
      blocks$,
      (camperCount, blocks) => ({ camperCount, blocks })
    )
      .subscribe(
        ({ camperCount, blocks }) => {
          res.render('challengeMap/show', {
            blocks,
            daysRunning,
            camperCount,
            title: "A map of all Free Code Camp's Challenges"
          });
        },
        next
      );
  }
};
<|endoftext|>


<commit_before>var RegExpTree = require('regexp-tree');
var regExpTree = new RegExpTree();

module.exports = (function () {
	'use strict';

	function RegExpGen () {}

	RegExpGen.prototype.gen = function (regexp) {
		var tree = regExpTree.parse(regexp);
	};

	return RegExpGen;
})();
<commit_msg>Temporary save<commit_after>var RegExpTree = require('regexp-tree');
var regExpTree = new RegExpTree();

module.exports = (function () {
	'use strict';

	function RegExpGen () {}

	RegExpGen.prototype.gen = function (regexp) {
		var tree = regExpTree.parse(regexp);

		return this.genGroup(regexp);
	};

	RegExpGen.prototype.genGroup = function (prevGenerated, group) {
		var sequences = group.sequences;
		var currGenerated = [];

		if (typeof sequences === 'string') {
			currGenerated.push(sequences);
		}
		else {
			var sequence;

			for (var i = 0, len = sequences.length; i < len; i++) {
				sequence = sequences[i];

				if (sequence.isGroup === true) {
					currGenerated.push(
						this.genGroup(prevGenerated, currGenerated)
					);
				}
				else if (sequence.isSet === true) {
					currGenerated.push(
						this.genSet(prevGenerated, currGenerated)
					);
				}
			}
		}

		return this.mergeGen(prevGenerated, currGenerated);
	};

	RegExpGen.prototype.genSet = function (prevGenerated, set) {

	};

	RegExpGen.prototype.mergeGen = function (prevGenerated, currGenerated) {
		var generated = [];

		for (var i = 0, lenPrev = prevGenerated.length; i < lenPrev; i++) {
			for (var k = 0, lenCurr = currGenerated.length; k < lenCurr; k++) {
				generated.push(prevGenerated[i] + currGenerated[k]);
			}
		}

		return generated;
	};

	return RegExpGen;
})();
<|endoftext|>


<commit_before><commit_msg>build<commit_after><|endoftext|>


<commit_before>const artists = {
  BrunoMars: {
    image: '../assets/bruno_mars.jpg',
    name: 'BRUNO MARS',
    song: 'Just The Way You Are',
    isNowPerforming: true,
    begin: '8:00pm',
    end: '9:00pm',
  },
  Samiami: {
    image: '../assets/19549522_10155631220882259_280100159_o.jpg',
    name: 'SAM WATKINSON',
    song: 'Help I\'m Locked In A Computer',
    isNowPerforming: true,
    begin: '2017',
    end: 'THE END OF TIME',
  },
  TheProdigy: {
    image: '../assets/TheProdigy(c)2009MickRock.jpg',
    name: 'THE PRODIGY',
    song: 'Firestarter',
    isNowPerforming: true,
    begin: '8:00pm',
    end: '9:00pm',
  }
};

function setArtistData (artistObject) {
  window.artistData = artistObject;
}

function setArtistDataFromQS () {
  artistKey = window.location.search.split('=')[1];

  setArtistData(artists[artistKey] || artists.Samiami);
}

setArtistDataFromQS();
<commit_msg>beef up query string parsing.<commit_after>const artists = {
  BrunoMars: {
    image: '../assets/bruno_mars.jpg',
    name: 'BRUNO MARS',
    song: 'Just The Way You Are',
    isNowPerforming: true,
    begin: '8:00pm',
    end: '9:00pm',
  },
  Samiami: {
    image: '../assets/19549522_10155631220882259_280100159_o.jpg',
    name: 'SAM WATKINSON',
    song: 'Help I\'m Locked In A Computer',
    isNowPerforming: true,
    begin: '2017',
    end: 'THE END OF TIME',
  },
  TheProdigy: {
    image: '../assets/TheProdigy(c)2009MickRock.jpg',
    name: 'THE PRODIGY',
    song: 'Firestarter',
    isNowPerforming: true,
    begin: '8:00pm',
    end: '9:00pm',
  }
};

function setArtistData (artistObject) {
  window.artistData = artistObject;
}

function setArtistDataFromQS () {
  const params = window.location.search.slice(1).split('&').reduce((paramObj, queryComponent) => {
    [key, value] = queryComponent.split('=');
    paramObj[key] = value;
    return paramObj;
  });

  const artistName = params['key'];

  setArtistData(artists[artistName] || artists.Samiami);
}

setArtistDataFromQS();
<|endoftext|>


<commit_before>/**
 * @file commandHandler
 * @author Sankarsan Kampa (a.k.a k3rn31p4nic)
 * @license MIT
 */

const parseArgs = require('command-line-args');
const COLOR = require('chalk');
const activeUsers = {};

/**
 * Handles Bastion's commands
 * @param {Message} message Discord.js message object
 * @returns {void}
 */
module.exports = async message => {
  try {
    let guild = await message.client.db.get(`SELECT prefix, language, ignoredChannels, ignoredRoles FROM guildSettings WHERE guildID=${message.guild.id}`);

    // Add guild's prefix to the discord.js guild object to minimize database reads.
    if (!message.guild.prefix || message.guild.prefix.join(' ') !== guild.prefix) {
      message.guild.prefix = guild.prefix.trim().split(' ');
    }
    // Add guild's language to the discord.js guild object to minimize database reads.
    if (!message.guild.language || message.guild.language !== guild.language) {
      message.guild.language = guild.language;
    }

    // The prefix used by the user to call the command.
    let usedPrefix;
    if (!message.guild.prefix.some(prefix => message.content.startsWith(usedPrefix = prefix))) return;

    // Ignore commands from ignored roles & channels. Doesn't affect the guild administrator.
    if (!message.member.hasPermission('ADMINISTRATOR')) {
      if (guild.ignoredChannels) {
        if (guild.ignoredChannels.split(' ').includes(message.channel.id)) return;
      }

      if (guild.ignoredRoles) {
        let ignoredRoles = guild.ignoredRoles.split(' ');
        for (let roleID of ignoredRoles) {
          if (message.member.roles.has(roleID)) return;
        }
      }
    }

    /**
     * @var {String} args The arguments used with the command.
     * @var {String} command The command name.
     */
    let args = message.content.split(' ');
    let command = args.shift().slice(usedPrefix.length).toLowerCase();

    // Resolves command name to the actual command (if any).
    let cmd;
    if (message.client.commands.has(command)) {
      cmd = message.client.commands.get(command);
    }
    else if (message.client.aliases.has(command)) {
      cmd = message.client.commands.get(message.client.aliases.get(command).toLowerCase());
    }
    else return;

    /**
     * @var {String} mdl The module that the command belongs to.
     */
    let mdl = cmd.config.module;

    /**
     * Command log messages
     */
    message.client.log.console(`\n[${new Date()}]`);
    message.client.log.console(COLOR.green('[COMMAND]: ') + usedPrefix + command);
    message.client.log.console(COLOR.green('[ARGUMENTs]: ') + (args.join(' ') || COLOR.yellow('No arguments to execute')));
    message.client.log.console(COLOR.green('[MODULE]: ') + mdl);
    if (message.client.shard) {
      message.client.log.console(`${COLOR.green('[SHARD]:')} ${message.client.shard.id}`);
    }
    message.client.log.console(`${COLOR.green('[SERVER]:')} ${message.guild} ${COLOR.cyan(`<#${message.guild.id}>`)}`);
    message.client.log.console(`${COLOR.green('[CHANNEL]:')} #${message.channel.name} ${COLOR.cyan(message.channel)}`);
    message.client.log.console(`${COLOR.green('[USER]:')} ${message.author.tag} ${COLOR.cyan(`${message.author}`)}`);

    if (!cmd.config.enabled) {
      return message.client.log.info('This command is disabled.');
    }

    /**
     * Bastion's hidden way of allowing users to enable/disable commands in a
     * specific channel.
     */
    if (message.channel.topic) {
      let parsedTopic = message.channel.topic.split('\n').filter(str => str.length);
      parsedTopic = parsedTopic[parsedTopic.length - 1];

      let topicSyntax = new RegExp(`${message.client.user.id}:(?:(?:enableCommands|disableCommands):[a-z ]+|(?:enableModules|disableModules):[a-z ]+)`, 'i');

      if (topicSyntax.test(parsedTopic)) {
        parsedTopic = parsedTopic.toLowerCase().split(':');
        if (parsedTopic.length >= 3) {
          let filteredCommands = parsedTopic[2].split(' ');

          switch (parsedTopic[1]) {
            case 'enablecommands':
              if (!filteredCommands.includes(command)) {
                return message.client.log.info('This command is disabled via channel topic.');
              }
              break;
            case 'disablecommands':
              if (filteredCommands.includes(command) || filteredCommands.includes('all')) {
                return message.client.log.info('This command is disabled via channel topic.');
              }
              break;
            case 'enablemodules':
              if (!filteredCommands.includes(mdl.toLowerCase())) {
                return message.client.log.info('This module is disabled via channel topic.');
              }
              break;
            case 'disablemodules':
              if (filteredCommands.includes(mdl.toLowerCase()) || filteredCommands.includes('all')) {
                return message.client.log.info('This module is disabled via channel topic.');
              }
              break;
            default:
              break;
          }
        }
      }
    }

    /**
     * Command cooldown handler
     */
    if (cmd.config.userCooldown && typeof cmd.config.userCooldown === 'number' && cmd.config.userCooldown >= 1 && cmd.config.userCooldown <= 1440) {
      if (!activeUsers.hasOwnProperty(cmd.help.name)) {
        activeUsers[cmd.help.name] = [];
      }
      if (activeUsers[cmd.help.name].includes(message.author.id)) {
        /**
         * Error condition is encountered.
         * @fires error
         */
        return message.client.emit('error', message.client.strings.error(message.guild.language, 'cooldown'), message.client.strings.error(message.guild.language, 'cooldown', true, `<@${message.author.id}>`, cmd.help.name, cmd.config.userCooldown), message.channel);
      }
    }

    let isSuccessRun = await cmd.run(message.client, message, parseArgs(cmd.config.argsDefinitions, { argv: args, partial: true }));

    if (isSuccessRun === true) {
      if (activeUsers.hasOwnProperty(cmd.help.name)) {
        activeUsers[cmd.help.name].push(message.author.id);
        message.client.setTimeout(() => {
          activeUsers.splice(activeUsers.indexOf(message.author.id), 1);
        }, cmd.config.userCooldown * 1000);
      }
    }
  }
  catch (e) {
    message.client.log.error(e);
  }
};
<commit_msg>Added new permission handler for user & bot perms<commit_after>/**
 * @file commandHandler
 * @author Sankarsan Kampa (a.k.a k3rn31p4nic)
 * @license MIT
 */

const parseArgs = require('command-line-args');
const COLOR = require('chalk');
const activeUsers = {};

/**
 * Handles Bastion's commands
 * @param {Message} message Discord.js message object
 * @returns {void}
 */
module.exports = async message => {
  try {
    let guild = await message.client.db.get(`SELECT prefix, language, ignoredChannels, ignoredRoles FROM guildSettings WHERE guildID=${message.guild.id}`);

    // Add guild's prefix to the discord.js guild object to minimize database reads.
    if (!message.guild.prefix || message.guild.prefix.join(' ') !== guild.prefix) {
      message.guild.prefix = guild.prefix.trim().split(' ');
    }
    // Add guild's language to the discord.js guild object to minimize database reads.
    if (!message.guild.language || message.guild.language !== guild.language) {
      message.guild.language = guild.language;
    }

    // The prefix used by the user to call the command.
    let usedPrefix;
    if (!message.guild.prefix.some(prefix => message.content.startsWith(usedPrefix = prefix))) return;

    // Ignore commands from ignored roles & channels. Doesn't affect the guild administrator.
    if (!message.member.hasPermission('ADMINISTRATOR')) {
      if (guild.ignoredChannels) {
        if (guild.ignoredChannels.split(' ').includes(message.channel.id)) return;
      }

      if (guild.ignoredRoles) {
        let ignoredRoles = guild.ignoredRoles.split(' ');
        for (let roleID of ignoredRoles) {
          if (message.member.roles.has(roleID)) return;
        }
      }
    }

    /**
     * @var {String} args The arguments used with the command.
     * @var {String} command The command name.
     */
    let args = message.content.split(' ');
    let command = args.shift().slice(usedPrefix.length).toLowerCase();

    // Resolves command name to the actual command (if any).
    let cmd;
    if (message.client.commands.has(command)) {
      cmd = message.client.commands.get(command);
    }
    else if (message.client.aliases.has(command)) {
      cmd = message.client.commands.get(message.client.aliases.get(command).toLowerCase());
    }
    else return;

    /**
     * @var {String} mdl The module that the command belongs to.
     */
    let mdl = cmd.config.module;

    /**
     * Command log messages
     */
    message.client.log.console(`\n[${new Date()}]`);
    message.client.log.console(COLOR.green('[COMMAND]: ') + usedPrefix + command);
    message.client.log.console(COLOR.green('[ARGUMENTs]: ') + (args.join(' ') || COLOR.yellow('No arguments to execute')));
    message.client.log.console(COLOR.green('[MODULE]: ') + mdl);
    if (message.client.shard) {
      message.client.log.console(`${COLOR.green('[SHARD]:')} ${message.client.shard.id}`);
    }
    message.client.log.console(`${COLOR.green('[SERVER]:')} ${message.guild} ${COLOR.cyan(`<#${message.guild.id}>`)}`);
    message.client.log.console(`${COLOR.green('[CHANNEL]:')} #${message.channel.name} ${COLOR.cyan(message.channel)}`);
    message.client.log.console(`${COLOR.green('[USER]:')} ${message.author.tag} ${COLOR.cyan(`${message.author}`)}`);

    if (!cmd.config.enabled) {
      return message.client.log.info('This command is disabled.');
    }

    /**
     * Bastion's hidden way of allowing users to enable/disable commands in a
     * specific channel.
     */
    if (message.channel.topic) {
      let parsedTopic = message.channel.topic.split('\n').filter(str => str.length);
      parsedTopic = parsedTopic[parsedTopic.length - 1];

      let topicSyntax = new RegExp(`${message.client.user.id}:(?:(?:enableCommands|disableCommands):[a-z ]+|(?:enableModules|disableModules):[a-z ]+)`, 'i');

      if (topicSyntax.test(parsedTopic)) {
        parsedTopic = parsedTopic.toLowerCase().split(':');
        if (parsedTopic.length >= 3) {
          let filteredCommands = parsedTopic[2].split(' ');

          switch (parsedTopic[1]) {
            case 'enablecommands':
              if (!filteredCommands.includes(command)) {
                return message.client.log.info('This command is disabled via channel topic.');
              }
              break;
            case 'disablecommands':
              if (filteredCommands.includes(command) || filteredCommands.includes('all')) {
                return message.client.log.info('This command is disabled via channel topic.');
              }
              break;
            case 'enablemodules':
              if (!filteredCommands.includes(mdl.toLowerCase())) {
                return message.client.log.info('This module is disabled via channel topic.');
              }
              break;
            case 'disablemodules':
              if (filteredCommands.includes(mdl.toLowerCase()) || filteredCommands.includes('all')) {
                return message.client.log.info('This module is disabled via channel topic.');
              }
              break;
            default:
              break;
          }
        }
      }
    }

    /**
     * Command permissions handler
     */
    // Checks if the user has the required permission
    if (cmd.help.userPermission) {
      // Checks native Discord permissions
      if (Object.keys(message.client.permissions).includes(cmd.help.userPermission)) {
        if (!message.channel.permissionsFor(message.member).has(cmd.help.userPermission)) {
          /**
           * User has missing permissions.
           * @fires userMissingPermissions
           */
          return message.client.emit('userMissingPermissions', cmd.help.userPermission);
        }
      }
      // Checks bot owner permission
      else if (cmd.help.userPermission === 'BOT_OWNER') {
        if (!message.client.credentials.ownerId.includes(message.author.id)) {
          /**
           * User has missing permissions.
           * @fires userMissingPermissions
           */
          return message.client.emit('userMissingPermissions', cmd.help.userPermission);
        }
      }
    }

    // Checks if Bastion has the required permission
    if (cmd.help.botPermission) {
      if (!message.guild.me.hasPermission(cmd.help.botPermission)) {
        /**
        * Bastion has missing permissions.
        * @fires bastionMissingPermissions
        */
        return message.client.emit('bastionMissingPermissions', cmd.help.botPermission, message);
      }
    }

    /**
     * Command cooldown handler
     */
    if (cmd.config.userCooldown && typeof cmd.config.userCooldown === 'number' && cmd.config.userCooldown >= 1 && cmd.config.userCooldown <= 1440) {
      if (!activeUsers.hasOwnProperty(cmd.help.name)) {
        activeUsers[cmd.help.name] = [];
      }
      if (activeUsers[cmd.help.name].includes(message.author.id)) {
        /**
         * Error condition is encountered.
         * @fires error
         */
        return message.client.emit('error', message.client.strings.error(message.guild.language, 'cooldown'), message.client.strings.error(message.guild.language, 'cooldown', true, `<@${message.author.id}>`, cmd.help.name, cmd.config.userCooldown), message.channel);
      }
    }

    let isSuccessRun = await cmd.run(message.client, message, parseArgs(cmd.config.argsDefinitions, { argv: args, partial: true }));

    if (isSuccessRun === true) {
      if (activeUsers.hasOwnProperty(cmd.help.name)) {
        activeUsers[cmd.help.name].push(message.author.id);
        message.client.setTimeout(() => {
          activeUsers.splice(activeUsers.indexOf(message.author.id), 1);
        }, cmd.config.userCooldown * 1000);
      }
    }
  }
  catch (e) {
    message.client.log.error(e);
  }
};
<|endoftext|>


<commit_before>const userAgent = navigator.userAgent;

const vrSupported = () => {
  return navigator.getVRDisplays !== undefined;
};

const checkHasExternalDisplay = () => (
  new Promise((resolve) => {
    if (!vrSupported()) {
      resolve(false);
      return;
    }
    navigator
      .getVRDisplays()
      .then(
        (displays) => {
          resolve(
            !!displays[0] &&
            !!displays[0].capabilities &&
            !!displays[0].capabilities.hasExternalDisplay
          );
        }
      )
      .catch((error) => {
        console.log(error);
        resolve(false);
      });
  })
);

const checkHasVR = () => (
  new Promise((resolve) => {
    if (!vrSupported()) {
      resolve(false);
      return;
    }
    navigator
      .getVRDisplays()
      .then(() => {
        resolve(true);
      })
      .catch((error) => {
        console.log(error);
        resolve(false);
      });
  })
);

const checkHas6DOF = () => (
  new Promise((resolve) => {
    if (!vrSupported()) {
      resolve(false);
      return;
    }
    navigator
      .getVRDisplays()
      .then((displays) => {
        resolve(
          !!displays[0] &&
          !!displays[0].capabilities &&
          !!displays[0].capabilities.hasPosition &&
          !!displays[0].capabilities.hasOrientation
        );
      })
      .catch((error) => {
        console.log(error);
        resolve(false);
      });
  })
);

const getVRDisplayName = () => (
  new Promise((resolve) => {
    if (!vrSupported()) {
      resolve(false);
      return;
    }
    navigator
      .getVRDisplays()
      .then((displays) => {
        resolve(displays[0].displayName);
      })
      .catch((error) => {
        console.log(error);
        resolve(false);
      });
  })
);
const isAndroid = /android/i.test(userAgent);
const feature = {
  isMobile: /android|ipad|iphone|iemobile/i.test(userAgent),
  isTablet: (isAndroid && !/mobile/i.test(userAgent)) // https://stackoverflow.com/questions/5341637/how-do-detect-android-tablets-in-general-useragent
    || /ipad/i.test(userAgent),
  isAndroid,
  isChrome: /chrome/i.test(userAgent),
  stats: /fps/.test(window.location.hash),
  prepare: () => (
    Promise.all([
      checkHasExternalDisplay()
        .then((hasExternalDisplay) => {
          feature.hasExternalDisplay = hasExternalDisplay;
        }),
      checkHasVR()
        .then((hasVR) => {
          feature.hasVR = !feature.isTablet && hasVR;
        }),
      checkHas6DOF()
        .then((has6DOF) => {
          feature.has6DOF = has6DOF;
        }),
      getVRDisplayName()
        .then((displayName) => {
          //  Expecting "Google, Inc. Daydream View".
          //  Unclear if stand-alone Daydream just announced at I/O 2017
          //  will eventually require its own displayName check.
          feature.isDaydream = /daydream/i.test(displayName);
          //  Expecting "HTC Vive DVT".
          feature.isVive = /vive/i.test(displayName);
          //  Expecting "Oculus VR HMD (HMD)" or "Oculus VR HMD (Sensor)".
          //  Note that "Rift" is NOT part of the displayName.
          feature.isOculus = /oculus/i.test(displayName);
          //  If it’s mobile but it’s not Daydream then we can consider
          //  it to be “Cardbaord” since we’re using the WebVR Polyfill
          //  to make any mobile device (with accelerometers) a potential
          //  3DOF virtual reality device.
          feature.isCardboard = feature.isMobile && !feature.isDaydream;
        }),
    ])
  ),
};

export default feature;
<commit_msg>remove check of missing VR capability. Fixes #309<commit_after>const userAgent = navigator.userAgent;

const vrSupported = () => {
  return navigator.getVRDisplays !== undefined;
};

const checkHasExternalDisplay = () => (
  new Promise((resolve) => {
    if (!vrSupported()) {
      resolve(false);
      return;
    }
    navigator
      .getVRDisplays()
      .then(
        (displays) => {
          resolve(
            !!displays[0] &&
            !!displays[0].capabilities &&
            !!displays[0].capabilities.hasExternalDisplay
          );
        }
      )
      .catch((error) => {
        console.log(error);
        resolve(false);
      });
  })
);

const checkHasVR = () => (
  new Promise((resolve) => {
    if (!vrSupported()) {
      resolve(false);
      return;
    }
    navigator
      .getVRDisplays()
      .then(() => {
        resolve(true);
      })
      .catch((error) => {
        console.log(error);
        resolve(false);
      });
  })
);

const checkHas6DOF = () => (
  new Promise((resolve) => {
    if (!vrSupported()) {
      resolve(false);
      return;
    }
    navigator
      .getVRDisplays()
      .then((displays) => {
        resolve(
          !!displays[0] &&
          !!displays[0].capabilities &&
          !!displays[0].capabilities.hasPosition
        );
      })
      .catch((error) => {
        console.log(error);
        resolve(false);
      });
  })
);

const getVRDisplayName = () => (
  new Promise((resolve) => {
    if (!vrSupported()) {
      resolve(false);
      return;
    }
    navigator
      .getVRDisplays()
      .then((displays) => {
        resolve(displays[0].displayName);
      })
      .catch((error) => {
        console.log(error);
        resolve(false);
      });
  })
);
const isAndroid = /android/i.test(userAgent);
const feature = {
  isMobile: /android|ipad|iphone|iemobile/i.test(userAgent),
  isTablet: (isAndroid && !/mobile/i.test(userAgent)) // https://stackoverflow.com/questions/5341637/how-do-detect-android-tablets-in-general-useragent
    || /ipad/i.test(userAgent),
  isAndroid,
  isChrome: /chrome/i.test(userAgent),
  stats: /fps/.test(window.location.hash),
  prepare: () => (
    Promise.all([
      checkHasExternalDisplay()
        .then((hasExternalDisplay) => {
          feature.hasExternalDisplay = hasExternalDisplay;
        }),
      checkHasVR()
        .then((hasVR) => {
          feature.hasVR = !feature.isTablet && hasVR;
        }),
      checkHas6DOF()
        .then((has6DOF) => {
          feature.has6DOF = has6DOF;
        }),
      getVRDisplayName()
        .then((displayName) => {
          //  Expecting "Google, Inc. Daydream View".
          //  Unclear if stand-alone Daydream just announced at I/O 2017
          //  will eventually require its own displayName check.
          feature.isDaydream = /daydream/i.test(displayName);
          //  Expecting "HTC Vive DVT".
          feature.isVive = /vive/i.test(displayName);
          //  Expecting "Oculus VR HMD (HMD)" or "Oculus VR HMD (Sensor)".
          //  Note that "Rift" is NOT part of the displayName.
          feature.isOculus = /oculus/i.test(displayName);
          //  If it’s mobile but it’s not Daydream then we can consider
          //  it to be “Cardbaord” since we’re using the WebVR Polyfill
          //  to make any mobile device (with accelerometers) a potential
          //  3DOF virtual reality device.
          feature.isCardboard = feature.isMobile && !feature.isDaydream;
        }),
    ])
  ),
};

export default feature;
<|endoftext|>


<commit_before>// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

var App = angular.module('App', ['ngRoute', 'ngMaterial', 'ngMessages', 'angularFileUpload']);

App.config(function($routeProvider) {
  $routeProvider.when('/settings', {
    controller: 'SettingsController',
    templateUrl: '/partials/settings.html',
    resolve: {
      settings: function($rootScope, $http, $q, $mdToast) {
        $mdToast.show(
          $mdToast.simple().textContent('Loading...')
        );
        var deferred = $q.defer();
        $http.get('/api/settings').then(function(response) {
          $mdToast.hide();
          deferred.resolve(response.data);
        });
        return deferred.promise;
      }
    }
  });

  $routeProvider.when('/projects/', {
    controller: 'ProjectsController',
    templateUrl: '/partials/projects/index.html',
    resolve: {
      projects: function($rootScope, $http, $q, $mdToast, $route) {
        $mdToast.show(
          $mdToast.simple().textContent('Loading...')
        );
        var deferred = $q.defer();
        var cursor = $route.current.params.pc;
        $http.get('/api/projects', { params: { pc: cursor } }).then(function(response) {
          $mdToast.hide();
          deferred.resolve(response.data);
        });
        return deferred.promise;
      }
    }
  });

  $routeProvider.when('/projects/new', {
    controller: 'NewProjectController',
    templateUrl: '/partials/projects/new.html',
  });

  $routeProvider.when('/projects/:id/edit', {
    controller: 'EditProjectController',
    templateUrl: '/partials/projects/edit.html',
    resolve: {
      project: function($http, $q, $mdToast, $route) {
        $mdToast.show(
          $mdToast.simple().textContent('Loading...')
        );
        var deferred = $q.defer();
        $http.get('/api/projects/' + $route.current.params.id).then(function(response) {
          $mdToast.hide();
          deferred.resolve(response.data);
        });
        return deferred.promise;
      }
    }
  });

  $routeProvider.when('/projects/:id/log', {
    controller: 'LogProjectController',
    templateUrl: '/partials/projects/log.html',
    resolve: {
      projectLoggers: function($http, $q, $mdToast, $route) {
        $mdToast.show(
          $mdToast.simple().textContent('Loading...')
        );
        var deferred = $q.defer();
        var cursor = $route.current.params.lc;
        $http.get('/api/projects/' + $route.current.params.id + '/log', { params: { lc: cursor } }).then(function(response) {
          $mdToast.hide();
          deferred.resolve(response.data);
        });
        return deferred.promise;
      }
    }
  });

  $routeProvider.otherwise({
    redirectTo: '/projects/'
  });
});

App.controller('AppController', function($scope, $location) {
  $scope.new = function() {
    $location.path('/projects/new');
  };

  $scope.settings = function() {
    $location.path('/settings');
  };
});

App.controller('SettingsController', function($scope, $rootScope, $window, $http, $location, $mdToast, settings) {
  $scope.settings = settings;
  $scope.clonedSettings = angular.copy($scope.settings);

  $scope.back = function() {
    $window.history.back();
  };

  $scope.save = function() {
    $mdToast.show(
      $mdToast.simple().textContent('Saving...')
    );
    $http.put('/api/settings', $scope.clonedSettings).then(function(response) {
      $mdToast.hide().then(function() {
        location.reload();
      });
    });
  };
});

App.controller('ProjectsController', function($scope, $route, $rootScope, $http, $location, $mdToast, projects) {
  $scope.projects = projects['entities'];
  $scope.hasPrevious = projects['hasPrevious'];
  $scope.hasNext = projects['hasNext'];
  $scope.previousCursor = projects['previousCursor'];
  $scope.nextCursor = projects['nextCursor'];

  $scope.previous = function() {
    $location.search('pc', $scope.previousCursor);
  };

  $scope.next = function() {
    $location.search('pc', $scope.nextCursor);
  };

  $scope.edit = function(project) {
    $location.path('/projects/' + project.id + '/edit');
  };

  $scope.delete = function(project) {
    $mdToast.show(
      $mdToast.simple().textContent('Deleting project ' + project.id + '...')
    );
    $http.delete('/api/projects/' + project.id).then(function(response) {
      for (var i = 0; i < $scope.projects.length; i++) {
        if ($scope.projects[i].id == project.id) {
          $scope.projects.splice(i, 1);
          break;
        }
      }
      $mdToast.hide();
    });
  };
});

App.controller('NewProjectController', function($scope, $rootScope, $http, $location, $mdToast) {
  $scope.save = function() {
    var project = {
      name: $scope.name,
      profileId: $scope.profileId,
    };

    $mdToast.show(
      $mdToast.simple().textContent('Saving...')
    );

    gapi.load('auth2', function() {
      var auth2 = gapi.auth2.init({
        'client_id': BulkUploader.CLIENT_ID,
        'scope': 'https://www.googleapis.com/auth/dfatrafficking'
      });

      auth2.grantOfflineAccess().then(function(result) {
        if (result['error']) {
          $mdToast.show($mdToast.simple().textContent(result['error']));
        } else {
          project['code'] = result['code'];
          $http.post('/api/projects', project).then(function(response) {
            $mdToast.hide().then(function() {
              $location.path('/projects/' + response.data.id + '/edit');
            });
          }, function(failure) {
            $mdToast.show(
              $mdToast.simple().textContent('Something went wrong! Check your settings.')
            );
          });
        }
      });
    });
  };
});

App.controller('EditProjectController', function($scope, $route, $window, $rootScope, $http, $routeParams, $location, $timeout, $mdToast, $mdDialog, FileUploader, project) {
  $scope.project = project;
  $scope.clonedProject = angular.copy($scope.project);

  var checkStatus = function() {
    $http.get('/api/projects/' + $scope.project.id + '/status').then(function(response) {
      var oldStatus = $scope.status;
      var newStatus = response.data.status;

      if ((oldStatus != 'COMPLETED' && !!oldStatus) && newStatus == 'COMPLETED') {
        $mdToast.show(
          $mdToast.simple().textContent('Completed!')
        );
      }

      if ((oldStatus != 'ERROR' && !!oldStatus) && newStatus == 'ERROR') {
        $mdToast.show(
          $mdToast.simple().textContent('Error!')
        );
      }

      $scope.status = newStatus;
      nextLoad();
    });
  };

  var loadPromise;

  var cancelNextLoad = function() {
    $timeout.cancel(loadPromise);
  };

  var nextLoad = function() {
    cancelNextLoad();
    loadPromise = $timeout(checkStatus, 5000);
  };

  checkStatus();

  $scope.$on('$destroy', function() {
    cancelNextLoad();
  });

  $scope.feedUploader = new FileUploader({
    autoUpload: true,
    removeAfterUpload: true,
    queueLimit: 1,
    url: $scope.project.feedUploadUrl
  });

  $scope.feedUploader.filters.push({
    name: 'csvFilter',
    fn: function(item, options) {
      if (item.type == 'text/csv') {
        return true;
      } else {
        return false;
      }
    }
  });

  $scope.feedUploader.onWhenAddingFileFailed = function(item, filter, options) {
    if (filter.name == 'csvFilter') {
      $mdToast.show(
        $mdToast.simple().textContent('The feed needs to be a CSV!')
      );
    }
  };

  $scope.feedUploader.onCompleteAll = function() {
    $route.reload();
  };

  $scope.assetsUploader = new FileUploader({
    autoUpload: true,
    removeAfterUpload: true
  });

  $scope.originalAssetsUploadItemFn = $scope.assetsUploader.uploadItem;

  $scope.assetsUploader.uploadItem = function() {
    var t = this;
    var args = arguments;
    $http.get('/api/projects/' + $scope.project.id + '/asset_upload_url').then(function(response) {
      $scope.assetsUploader.onBeforeUploadItem = function(item) {
        item.url = response.data.uploadUrl;
      };
    }).then(function() {
      $scope.originalAssetsUploadItemFn.apply(t, args);
    });
  };

  $scope.assetsUploader.onCompleteAll = function() {
    $route.reload();
  };

  $scope.startRun = function() {
    $http.post('/api/projects/' + $scope.project.id + '/run').then(function(response) {
      checkStatus();

      $mdToast.show(
        $mdToast.simple()
          .textContent('Running...')
          .action('Cancel')
          .highlightAction(true)
      ).then(function(response) {
        if (response == 'ok') {
          $scope.cancelRun();
        }
      });
    });
  };

  $scope.cancelRun = function() {
    $http.delete('/api/projects/' + $scope.project.id + '/run').then(function(response) {
      checkStatus();

      $mdToast.show(
        $mdToast.simple().textContent('Cancelling...')
      );
    });
  };

  $scope.downloadFeed = function() {
    if ($scope.clonedProject.feed) {
      $window.open('/api/projects/' + $scope.project.id + '/feed', '_blank');
    }
  };

  $scope.removeFeed = function() {
    $scope.clonedProject.feed = '';
    $scope.update();
  };

  $scope.removeAsset = function(asset) {
    for (var i = 0; i < $scope.clonedProject.assets.length; i++) {
      if ($scope.clonedProject.assets[i]['key'] == asset['key']) {
        $scope.clonedProject.assets.splice(i, 1);
        break;
      }
    }

    $scope.update();
  };

  $scope.showSheetsDialog = function($event) {
    $event.preventDefault();

    var dialog = $mdDialog.prompt()
      .title('Google Sheet URL')
      .textContent('Please enter the URL of the Google Sheet you would like to import.')
      .placeholder('Google Sheet URL')
      .ariaLabel('Google Sheet URL')
      .targetEvent($event)
      .required(true)
      .ok('Import')
      .cancel('Cancel');

    $mdDialog.show(dialog).then(function(result) {
      var urlMatch = result.trim().match(/^https?:\/\/.*?.google.com\/.*\/d\/(.*?)\//);

      if (urlMatch) {
        var sheetId = urlMatch[1];

        gapi.load('client:auth2', function() {
          gapi.client.init({
            'clientId': BulkUploader.CLIENT_ID,
            'discoveryDocs': ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'],
            'scope': 'https://www.googleapis.com/auth/drive.readonly',
          }).then(function() {
            gapi.auth2.getAuthInstance().signIn().then(function() {
              gapi.client.load('drive', 'v3', function() {
                gapi.client.drive.files.export({
                  fileId: sheetId,
                  mimeType: 'text/csv'
                }).then(function(response) {
                  var file = new File(response.body.split('\n'), 'feed.csv', { type: 'text/csv' });
                  $scope.feedUploader.addToQueue(file);
                }, function(response) {
                  $mdToast.show(
                    $mdToast.simple().textContent(response.result.error.message)
                  );
                });
              });
            });
          });
        });
      } else {
        $mdToast.show(
          $mdToast.simple().textContent("Oops! That URL doesn't look right. Try another.")
        );
      }
    },
    function() {
      // Do nothing.
    });
  };

  $scope.update = function() {
    $mdToast.show(
      $mdToast.simple().textContent('Updating...')
    );
    $http.put('/api/projects/' + $scope.project.id, $scope.clonedProject).then(function(response) {
      $mdToast.hide().then(function() {
        $route.reload();
      });
    });
  };

  $scope.log = function() {
    $location.path('/projects/' + project.id + '/log');
  };

  $scope.sdf = function() {
    console.log('SDF!');
  };
});

App.controller('LogProjectController', function($scope, $route, $http, $timeout, $location, projectLoggers) {
  $scope.projectLoggers = projectLoggers['entities'];
  $scope.projectName = $scope.projectLoggers[0].projectName;
  $scope.projectId = $scope.projectLoggers[0].projectId;
  $scope.hasPrevious = projectLoggers['hasPrevious'];
  $scope.hasNext = projectLoggers['hasNext'];
  $scope.previousCursor = projectLoggers['previousCursor'];
  $scope.nextCursor = projectLoggers['nextCursor'];

  $scope.previous = function() {
    $location.search('lc', $scope.previousCursor);
  };

  $scope.next = function() {
    $location.search('lc', $scope.nextCursor);
  };

  var polling = function() {
    var cursor = $route.current.params.lc;
    $http.get('/api/projects/' + $scope.projectId + '/log', { params: { lc: cursor } }).then(function(response) {
      $scope.projectLoggers = response.data['entities'];
      nextLoad();
    });
  };

  var loadPromise;

  var cancelNextLoad = function() {
    $timeout.cancel(loadPromise);
  };

  var nextLoad = function() {
    cancelNextLoad();
    loadPromise = $timeout(polling, 5000);
  };

  polling();

  $scope.$on('$destroy', function() {
    cancelNextLoad();
  });

  $scope.editProject = function() {
    $location.path('/projects/' + $scope.projectId + '/edit');
    $location.search('lc', undefined);
  };
});
<commit_msg>Formatted app.js and added better gapi auth2 structure<commit_after>// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

"use strict";

var App = angular.module("App", [
  "ngRoute",
  "ngMaterial",
  "ngMessages",
  "angularFileUpload"
]);

App.config(function($routeProvider) {
  $routeProvider.when("/settings", {
    controller: "SettingsController",
    templateUrl: "/partials/settings.html",
    resolve: {
      settings: function($rootScope, $http, $q, $mdToast) {
        $mdToast.show($mdToast.simple().textContent("Loading..."));
        var deferred = $q.defer();
        $http.get("/api/settings").then(function(response) {
          $mdToast.hide();
          deferred.resolve(response.data);
        });
        return deferred.promise;
      }
    }
  });

  $routeProvider.when("/projects/", {
    controller: "ProjectsController",
    templateUrl: "/partials/projects/index.html",
    resolve: {
      projects: function($rootScope, $http, $q, $mdToast, $route) {
        $mdToast.show($mdToast.simple().textContent("Loading..."));
        var deferred = $q.defer();
        var cursor = $route.current.params.pc;
        $http
          .get("/api/projects", { params: { pc: cursor } })
          .then(function(response) {
            $mdToast.hide();
            deferred.resolve(response.data);
          });
        return deferred.promise;
      }
    }
  });

  $routeProvider.when("/projects/new", {
    controller: "NewProjectController",
    templateUrl: "/partials/projects/new.html"
  });

  $routeProvider.when("/projects/:id/edit", {
    controller: "EditProjectController",
    templateUrl: "/partials/projects/edit.html",
    resolve: {
      project: function($http, $q, $mdToast, $route) {
        $mdToast.show($mdToast.simple().textContent("Loading..."));
        var deferred = $q.defer();
        $http
          .get("/api/projects/" + $route.current.params.id)
          .then(function(response) {
            $mdToast.hide();
            deferred.resolve(response.data);
          });
        return deferred.promise;
      }
    }
  });

  $routeProvider.when("/projects/:id/log", {
    controller: "LogProjectController",
    templateUrl: "/partials/projects/log.html",
    resolve: {
      projectLoggers: function($http, $q, $mdToast, $route) {
        $mdToast.show($mdToast.simple().textContent("Loading..."));
        var deferred = $q.defer();
        var cursor = $route.current.params.lc;
        $http
          .get("/api/projects/" + $route.current.params.id + "/log", {
            params: { lc: cursor }
          })
          .then(function(response) {
            $mdToast.hide();
            deferred.resolve(response.data);
          });
        return deferred.promise;
      }
    }
  });

  $routeProvider.otherwise({
    redirectTo: "/projects/"
  });
});

App.controller("AppController", function($scope, $location) {
  $scope.new = function() {
    $location.path("/projects/new");
  };

  $scope.settings = function() {
    $location.path("/settings");
  };
});

App.controller("SettingsController", function(
  $scope,
  $rootScope,
  $window,
  $http,
  $location,
  $mdToast,
  settings
) {
  $scope.settings = settings;
  $scope.clonedSettings = angular.copy($scope.settings);

  $scope.back = function() {
    $window.history.back();
  };

  $scope.save = function() {
    $mdToast.show($mdToast.simple().textContent("Saving..."));
    $http.put("/api/settings", $scope.clonedSettings).then(function(response) {
      $mdToast.hide().then(function() {
        location.reload();
      });
    });
  };
});

App.controller("ProjectsController", function(
  $scope,
  $route,
  $rootScope,
  $http,
  $location,
  $mdToast,
  projects
) {
  $scope.projects = projects["entities"];
  $scope.hasPrevious = projects["hasPrevious"];
  $scope.hasNext = projects["hasNext"];
  $scope.previousCursor = projects["previousCursor"];
  $scope.nextCursor = projects["nextCursor"];

  $scope.previous = function() {
    $location.search("pc", $scope.previousCursor);
  };

  $scope.next = function() {
    $location.search("pc", $scope.nextCursor);
  };

  $scope.edit = function(project) {
    $location.path("/projects/" + project.id + "/edit");
  };

  $scope.delete = function(project) {
    $mdToast.show(
      $mdToast.simple().textContent("Deleting project " + project.id + "...")
    );
    $http.delete("/api/projects/" + project.id).then(function(response) {
      for (var i = 0; i < $scope.projects.length; i++) {
        if ($scope.projects[i].id == project.id) {
          $scope.projects.splice(i, 1);
          break;
        }
      }
      $mdToast.hide();
    });
  };
});

App.controller("NewProjectController", function(
  $scope,
  $rootScope,
  $http,
  $location,
  $mdToast
) {
  $scope.save = function() {
    var project = {
      name: $scope.name,
      profileId: $scope.profileId
    };

    $mdToast.show($mdToast.simple().textContent("Saving..."));

    gapi.load("auth2", function() {
      var auth2 = gapi.auth2.init({
        clientId: BulkUploader.CLIENT_ID,
        discoveryDocs: [
          "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"
        ],
        scope:
          "https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/dfatrafficking"
      });

      auth2.grantOfflineAccess().then(function(result) {
        if (result["error"]) {
          $mdToast.show($mdToast.simple().textContent(result["error"]));
        } else {
          project["code"] = result["code"];
          $http.post("/api/projects", project).then(
            function(response) {
              $mdToast.hide().then(function() {
                $location.path("/projects/" + response.data.id + "/edit");
              });
            },
            function(failure) {
              $mdToast.show(
                $mdToast
                  .simple()
                  .textContent("Something went wrong! Check your settings.")
              );
            }
          );
        }
      });
    });
  };
});

App.controller("EditProjectController", function(
  $scope,
  $route,
  $window,
  $rootScope,
  $http,
  $routeParams,
  $location,
  $timeout,
  $mdToast,
  $mdDialog,
  FileUploader,
  project
) {
  $scope.project = project;
  $scope.clonedProject = angular.copy($scope.project);

  var checkStatus = function() {
    $http
      .get("/api/projects/" + $scope.project.id + "/status")
      .then(function(response) {
        var oldStatus = $scope.status;
        var newStatus = response.data.status;

        if (
          oldStatus != "COMPLETED" &&
          !!oldStatus &&
          newStatus == "COMPLETED"
        ) {
          $mdToast.show($mdToast.simple().textContent("Completed!"));
        }

        if (oldStatus != "ERROR" && !!oldStatus && newStatus == "ERROR") {
          $mdToast.show($mdToast.simple().textContent("Error!"));
        }

        $scope.status = newStatus;
        nextLoad();
      });
  };

  var loadPromise;

  var cancelNextLoad = function() {
    $timeout.cancel(loadPromise);
  };

  var nextLoad = function() {
    cancelNextLoad();
    loadPromise = $timeout(checkStatus, 5000);
  };

  checkStatus();

  $scope.$on("$destroy", function() {
    cancelNextLoad();
  });

  $scope.feedUploader = new FileUploader({
    autoUpload: true,
    removeAfterUpload: true,
    queueLimit: 1,
    url: $scope.project.feedUploadUrl
  });

  $scope.feedUploader.filters.push({
    name: "csvFilter",
    fn: function(item, options) {
      if (item.type == "text/csv") {
        return true;
      } else {
        return false;
      }
    }
  });

  $scope.feedUploader.onWhenAddingFileFailed = function(item, filter, options) {
    if (filter.name == "csvFilter") {
      $mdToast.show(
        $mdToast.simple().textContent("The feed needs to be a CSV!")
      );
    }
  };

  $scope.feedUploader.onCompleteAll = function() {
    $route.reload();
  };

  $scope.assetsUploader = new FileUploader({
    autoUpload: true,
    removeAfterUpload: true
  });

  $scope.originalAssetsUploadItemFn = $scope.assetsUploader.uploadItem;

  $scope.assetsUploader.uploadItem = function() {
    var t = this;
    var args = arguments;
    $http
      .get("/api/projects/" + $scope.project.id + "/asset_upload_url")
      .then(function(response) {
        $scope.assetsUploader.onBeforeUploadItem = function(item) {
          item.url = response.data.uploadUrl;
        };
      })
      .then(function() {
        $scope.originalAssetsUploadItemFn.apply(t, args);
      });
  };

  $scope.assetsUploader.onCompleteAll = function() {
    $route.reload();
  };

  $scope.startRun = function() {
    $http
      .post("/api/projects/" + $scope.project.id + "/run")
      .then(function(response) {
        checkStatus();

        $mdToast
          .show(
            $mdToast
              .simple()
              .textContent("Running...")
              .action("Cancel")
              .highlightAction(true)
          )
          .then(function(response) {
            if (response == "ok") {
              $scope.cancelRun();
            }
          });
      });
  };

  $scope.cancelRun = function() {
    $http
      .delete("/api/projects/" + $scope.project.id + "/run")
      .then(function(response) {
        checkStatus();

        $mdToast.show($mdToast.simple().textContent("Cancelling..."));
      });
  };

  $scope.downloadFeed = function() {
    if ($scope.clonedProject.feed) {
      $window.open("/api/projects/" + $scope.project.id + "/feed", "_blank");
    }
  };

  $scope.removeFeed = function() {
    $scope.clonedProject.feed = "";
    $scope.update();
  };

  $scope.removeAsset = function(asset) {
    for (var i = 0; i < $scope.clonedProject.assets.length; i++) {
      if ($scope.clonedProject.assets[i]["key"] == asset["key"]) {
        $scope.clonedProject.assets.splice(i, 1);
        break;
      }
    }

    $scope.update();
  };

  $scope.showSheetsDialog = function($event) {
    $event.preventDefault();

    var dialog = $mdDialog
      .prompt()
      .title("Google Sheet URL")
      .textContent(
        "Please enter the URL of the Google Sheet you would like to import."
      )
      .placeholder("Google Sheet URL")
      .ariaLabel("Google Sheet URL")
      .targetEvent($event)
      .required(true)
      .ok("Import")
      .cancel("Cancel");

    $mdDialog.show(dialog).then(
      function(result) {
        var urlMatch = result
          .trim()
          .match(/^https?:\/\/.*?.google.com\/.*\/d\/(.*?)\//);

        if (urlMatch) {
          var sheetId = urlMatch[1];

          gapi.load("client:auth2", function() {
            gapi.client
              .init({
                clientId: BulkUploader.CLIENT_ID,
                discoveryDocs: [
                  "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"
                ],
                scope:
                  "https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/dfatrafficking"
              })
              .then(function() {
                gapi.auth2
                  .getAuthInstance()
                  .signIn()
                  .then(function() {
                    gapi.client.load("drive", "v3", function() {
                      gapi.client.drive.files
                        .export({
                          fileId: sheetId,
                          mimeType: "text/csv"
                        })
                        .then(
                          function(response) {
                            var file = new File(
                              response.body.split("\n"),
                              "feed.csv",
                              { type: "text/csv" }
                            );
                            $scope.feedUploader.addToQueue(file);
                          },
                          function(response) {
                            $mdToast.show(
                              $mdToast
                                .simple()
                                .textContent(response.result.error.message)
                            );
                          }
                        );
                    });
                  });
              });
          });
        } else {
          $mdToast.show(
            $mdToast
              .simple()
              .textContent("Oops! That URL doesn't look right. Try another.")
          );
        }
      },
      function() {
        // Do nothing.
      }
    );
  };

  $scope.update = function() {
    $mdToast.show($mdToast.simple().textContent("Updating..."));
    $http
      .put("/api/projects/" + $scope.project.id, $scope.clonedProject)
      .then(function(response) {
        $mdToast.hide().then(function() {
          $route.reload();
        });
      });
  };

  $scope.log = function() {
    $location.path("/projects/" + project.id + "/log");
  };

  $scope.sdf = function() {
    console.log("SDF!");
  };
});

App.controller("LogProjectController", function(
  $scope,
  $route,
  $http,
  $timeout,
  $location,
  projectLoggers
) {
  $scope.projectLoggers = projectLoggers["entities"];
  $scope.projectName = $scope.projectLoggers[0].projectName;
  $scope.projectId = $scope.projectLoggers[0].projectId;
  $scope.hasPrevious = projectLoggers["hasPrevious"];
  $scope.hasNext = projectLoggers["hasNext"];
  $scope.previousCursor = projectLoggers["previousCursor"];
  $scope.nextCursor = projectLoggers["nextCursor"];

  $scope.previous = function() {
    $location.search("lc", $scope.previousCursor);
  };

  $scope.next = function() {
    $location.search("lc", $scope.nextCursor);
  };

  var polling = function() {
    var cursor = $route.current.params.lc;
    $http
      .get("/api/projects/" + $scope.projectId + "/log", {
        params: { lc: cursor }
      })
      .then(function(response) {
        $scope.projectLoggers = response.data["entities"];
        nextLoad();
      });
  };

  var loadPromise;

  var cancelNextLoad = function() {
    $timeout.cancel(loadPromise);
  };

  var nextLoad = function() {
    cancelNextLoad();
    loadPromise = $timeout(polling, 5000);
  };

  polling();

  $scope.$on("$destroy", function() {
    cancelNextLoad();
  });

  $scope.editProject = function() {
    $location.path("/projects/" + $scope.projectId + "/edit");
    $location.search("lc", undefined);
  };
});
<|endoftext|>