<commit_before>import { Connectors, debug, debugGroup, debugGroupEnd, runCallbacks, runCallbacksAsync } from 'meteor/vulcan:core';
import { createError } from 'apollo-errors';
import Votes from './votes/collection.js';
import Users from 'meteor/vulcan:users';
import { recalculateScore, recalculateBaseScore } from './scoring.js';

/*

Define voting operations

*/
const voteTypes = {}

/*

Add new vote types

*/
export const addVoteType = (voteType, voteTypeOptions) => {
  voteTypes[voteType] = voteTypeOptions;
}

addVoteType('upvote', {power: 1, exclusive: true});
addVoteType('downvote', {power: -1, exclusive: true});

/*

Test if a user has voted on the client

*/
export const hasVotedClient = ({ document, voteType }) => {
  const userVotes = document.currentUserVotes;
  if (voteType) {
    return _.where(userVotes, { voteType }).length
  } else {
    return userVotes && userVotes.length
  }
}

/*

Calculate total power of all a user's votes on a document

*/
const calculateTotalPower = votes => _.pluck(votes, 'power').reduce((a, b) => a + b, 0);

/*

Test if a user has voted on the server

*/
const hasVotedServer = async ({ document, voteType, user }) => {
  const vote = await Connectors.get(Votes, {documentId: document._id, userId: user._id, voteType});
  return vote;
}

/*

Add a vote of a specific type on the client

*/
const addVoteClient = ({ document, collection, voteType, user, voteId }) => {

  const newDocument = {
    ...document,
    baseScore: document.baseScore || 0,
    __typename: collection.options.typeName,
    currentUserVotes: document.currentUserVotes || [],
  };

  // create new vote and add it to currentUserVotes array
  const vote = createVote({ document, collectionName: collection.options.collectionName, voteType, user, voteId });
  newDocument.currentUserVotes = [...newDocument.currentUserVotes, vote];

  // increment baseScore
  newDocument.baseScore += vote.power;
  newDocument.score = recalculateScore(newDocument);

  return newDocument;
}

/*

Add a vote of a specific type on the server

*/
const addVoteServer = async (voteOptions) => {

  const { document, collection, voteType, user, voteId, updateDocument } = voteOptions;
  const newDocument = _.clone(document);

  // create vote and insert it
  const vote = createVote({ document, collectionName: collection.options.collectionName, voteType, user, voteId });
  delete vote.__typename;
  await Connectors.create(Votes, vote);

  // LESSWRONG – recalculateBaseScore
  newDocument.baseScore = recalculateBaseScore(newDocument)
  newDocument.score = recalculateScore(newDocument);

  if (updateDocument) {
    // update document score & set item as active
    await Connectors.update(collection, {_id: document._id}, {$set: {inactive: false, baseScore: newDocument.baseScore, score: newDocument.score}});
  }
  return {newDocument, vote};
}

/*

Cancel votes of a specific type on a given document (client)

*/
const cancelVoteClient = ({ document, voteType }) => {
  const vote = _.findWhere(document.currentUserVotes, { voteType });
  const newDocument = _.clone(document);
  if (vote) {
    // subtract vote scores
    newDocument.baseScore -= vote.power;
    newDocument.score = recalculateScore(newDocument);

    const newVotes = _.reject(document.currentUserVotes, vote => vote.voteType === voteType);

    // clear out vote of this type
    newDocument.currentUserVotes = newVotes;

  }
  return newDocument;
}

/*

Clear *all* votes for a given document and user (client)

*/
const clearVotesClient = ({ document }) => {
  const newDocument = _.clone(document);
  newDocument.baseScore -= calculateTotalPower(document.currentUserVotes);
  newDocument.score = recalculateScore(newDocument);
  newDocument.currentUserVotes = [];
  return newDocument
}

/*

Clear all votes for a given document and user (server)

*/
const clearVotesServer = async ({ document, user, collection, updateDocument }) => {
  const newDocument = _.clone(document);
  const votes = await Connectors.find(Votes, { documentId: document._id, userId: user._id});
  if (votes.length) {
    await Connectors.delete(Votes, {documentId: document._id, userId: user._id});
    votes.forEach((vote)=> {
      runCallbacksAsync(`votes.cancel.async`, {newDocument, vote}, collection, user);
    })
    if (updateDocument) {
      await Connectors.update(collection, {_id: document._id}, {$inc: {baseScore: recalculateBaseScore(document) }});
    }
    newDocument.baseScore = recalculateBaseScore(newDocument);
    newDocument.score = recalculateScore(newDocument);
  }
  return newDocument;
}

/*

Cancel votes of a specific type on a given document (server)

*/
export const cancelVoteServer = async ({ document, voteType, collection, user, updateDocument }) => {

  const newDocument = _.clone(document);
  const vote = Votes.findOne({documentId: document._id, userId: user._id, voteType})
  // remove vote object
  await Connectors.delete(Votes, {_id: vote._id});
  newDocument.baseScore = recalculateBaseScore(newDocument);
  newDocument.score = recalculateScore(newDocument);

  if (updateDocument) {
    // update document score
    await Connectors.update(
      collection,
      {_id: document._id},
      {$set: {
        inactive: false,
        score: newDocument.score,
        baseScore: newDocument.baseScore
      }}
    );
  }
  return {newDocument, vote};
}

/*

Determine a user's voting power for a given operation.
If power is a function, call it on user

*/
const getVotePower = ({ user, voteType, document }) => {
  const power = voteTypes[voteType] && voteTypes[voteType].power || 1;
  return typeof power === 'function' ? power(user, document) : power;
};

/*

Create new vote object

*/
const createVote = ({ document, collectionName, voteType, user, voteId }) => {

  const vote = {
    documentId: document._id,
    collectionName,
    userId: user._id,
    voteType: voteType,
    power: getVotePower({user, voteType, document}),
    votedAt: new Date(),
    __typename: 'Vote'
  }

  // when creating a vote from the server, voteId can sometimes be undefined
  if (voteId) vote._id = voteId;

  return vote;

};

/*

Optimistic response for votes

*/
export const performVoteClient = ({ document, collection, voteType = 'upvote', user, voteId }) => {

  const collectionName = collection.options.collectionName;
  let returnedDocument;

  // console.log('// voteOptimisticResponse')
  // console.log('collectionName: ', collectionName)
  // console.log('document:', document)
  // console.log('voteType:', voteType)

  // make sure item and user are defined
  if (!document || !user || !Users.canDo(user, `${collectionName.toLowerCase()}.${voteType}`)) {
    throw new Error(`Cannot perform operation '${collectionName.toLowerCase()}.${voteType}'`);
  }

  let voteOptions = {document, collection, voteType, user, voteId};

  if (hasVotedClient({document, voteType})) {

    // console.log('action: cancel')
    returnedDocument = cancelVoteClient(voteOptions);
    // returnedDocument = runCallbacks(`votes.cancel.client`, returnedDocument, collection, user);

  } else {

    // console.log('action: vote')

    if (voteTypes[voteType].exclusive) {
      voteOptions.document = clearVotesClient({document, collection, voteType, user, voteId})
    }

    returnedDocument = addVoteClient(voteOptions);
    returnedDocument = runCallbacks(`votes.${voteType}.client`, returnedDocument, collection, user, voteType);
  }

  // console.log('returnedDocument:', returnedDocument)

  return returnedDocument;
}

/*

Server-side database operation

### updateDocument
if set to true, this will perform its own database updates. If false, will only
return an updated document without performing any database operations on it.

*/
export const performVoteServer = async ({ documentId, document, voteType = 'bigUpvote', collection, voteId, user, updateDocument = true }) => {

  const collectionName = collection.options.collectionName;
  document = document || await Connectors.get(collection, documentId);

  debug('');
  debugGroup(`--------------- start \x1b[35mperformVoteServer\x1b[0m  ---------------`);
  debug('collectionName: ', collectionName);
  debug('document: ', document);
  debug('voteType: ', voteType);

  const voteOptions = {document, collection, voteType, user, voteId, updateDocument};

  if (!document || !user || !Users.canDo(user, `${collectionName.toLowerCase()}.${voteType}`)) {
    const VoteError = createError('voting.no_permission', {message: 'voting.no_permission'});
    throw new VoteError();
  }

  const existingVote = await hasVotedServer({document, voteType, user});

  if (existingVote) {

    // console.log('action: cancel')

    // runCallbacks(`votes.cancel.sync`, document, collection, user);
    let voteDocTuple = await cancelVoteServer(voteOptions);
    voteDocTuple = await runCallbacks(`votes.cancel.sync`, voteDocTuple, collection, user);
    document = voteDocTuple.newDocument;
    runCallbacksAsync(`votes.cancel.async`, voteDocTuple, collection, user);

  } else {

    // console.log('action: vote')

    if (voteTypes[voteType].exclusive) {
      document = await clearVotesServer(voteOptions)
    }

    let voteDocTuple = await addVoteServer({...voteOptions, document}); //Make sure to pass the new document to addVoteServer
    voteDocTuple = await runCallbacks(`votes.${voteType}.sync`, voteDocTuple, collection, user);
    document = voteDocTuple.newDocument;
    runCallbacksAsync(`votes.${voteType}.async`, voteDocTuple, collection, user);
  }

  debug('document after vote: ', document);
  debugGroupEnd();
  debug(`--------------- end \x1b[35m performVoteServer\x1b[0m ---------------`);
  debug('');

  // const newDocument = collection.findOne(documentId);
  document.__typename = collection.options.typeName;
  return document;

}
<commit_msg>vote.cancel.client and vote.clear.client callbacks<commit_after>import { Connectors, debug, debugGroup, debugGroupEnd, runCallbacks, runCallbacksAsync } from 'meteor/vulcan:core';
import { createError } from 'apollo-errors';
import Votes from './votes/collection.js';
import Users from 'meteor/vulcan:users';
import { recalculateScore, recalculateBaseScore } from './scoring.js';

/*

Define voting operations

*/
const voteTypes = {}

/*

Add new vote types

*/
export const addVoteType = (voteType, voteTypeOptions) => {
  voteTypes[voteType] = voteTypeOptions;
}

addVoteType('upvote', {power: 1, exclusive: true});
addVoteType('downvote', {power: -1, exclusive: true});

/*

Test if a user has voted on the client

*/
export const hasVotedClient = ({ document, voteType }) => {
  const userVotes = document.currentUserVotes;
  if (voteType) {
    return _.where(userVotes, { voteType }).length
  } else {
    return userVotes && userVotes.length
  }
}

/*

Calculate total power of all a user's votes on a document

*/
const calculateTotalPower = votes => _.pluck(votes, 'power').reduce((a, b) => a + b, 0);

/*

Test if a user has voted on the server

*/
const hasVotedServer = async ({ document, voteType, user }) => {
  const vote = await Connectors.get(Votes, {documentId: document._id, userId: user._id, voteType});
  return vote;
}

/*

Add a vote of a specific type on the client

*/
const addVoteClient = ({ document, collection, voteType, user, voteId }) => {

  const newDocument = {
    ...document,
    baseScore: document.baseScore || 0,
    __typename: collection.options.typeName,
    currentUserVotes: document.currentUserVotes || [],
  };

  // create new vote and add it to currentUserVotes array
  const vote = createVote({ document, collectionName: collection.options.collectionName, voteType, user, voteId });
  newDocument.currentUserVotes = [...newDocument.currentUserVotes, vote];

  // increment baseScore
  newDocument.baseScore += vote.power;
  newDocument.score = recalculateScore(newDocument);

  return newDocument;
}

/*

Add a vote of a specific type on the server

*/
const addVoteServer = async (voteOptions) => {

  const { document, collection, voteType, user, voteId, updateDocument } = voteOptions;
  const newDocument = _.clone(document);

  // create vote and insert it
  const vote = createVote({ document, collectionName: collection.options.collectionName, voteType, user, voteId });
  delete vote.__typename;
  await Connectors.create(Votes, vote);

  // LESSWRONG – recalculateBaseScore
  newDocument.baseScore = recalculateBaseScore(newDocument)
  newDocument.score = recalculateScore(newDocument);

  if (updateDocument) {
    // update document score & set item as active
    await Connectors.update(collection, {_id: document._id}, {$set: {inactive: false, baseScore: newDocument.baseScore, score: newDocument.score}});
  }
  return {newDocument, vote};
}

/*

Cancel votes of a specific type on a given document (client)

*/
const cancelVoteClient = ({ document, voteType }) => {
  const vote = _.findWhere(document.currentUserVotes, { voteType });
  const newDocument = _.clone(document);
  if (vote) {
    // subtract vote scores
    newDocument.baseScore -= vote.power;
    newDocument.score = recalculateScore(newDocument);

    const newVotes = _.reject(document.currentUserVotes, vote => vote.voteType === voteType);

    // clear out vote of this type
    newDocument.currentUserVotes = newVotes;

  }
  return newDocument;
}

/*

Clear *all* votes for a given document and user (client)

*/
const clearVotesClient = ({ document }) => {
  const newDocument = _.clone(document);
  newDocument.baseScore -= calculateTotalPower(document.currentUserVotes);
  newDocument.score = recalculateScore(newDocument);
  newDocument.currentUserVotes = [];
  return newDocument
}

/*

Clear all votes for a given document and user (server)

*/
const clearVotesServer = async ({ document, user, collection, updateDocument }) => {
  const newDocument = _.clone(document);
  const votes = await Connectors.find(Votes, { documentId: document._id, userId: user._id});
  if (votes.length) {
    await Connectors.delete(Votes, {documentId: document._id, userId: user._id});
    votes.forEach((vote)=> {
      runCallbacksAsync(`votes.cancel.async`, {newDocument, vote}, collection, user);
    })
    if (updateDocument) {
      await Connectors.update(collection, {_id: document._id}, {$inc: {baseScore: recalculateBaseScore(document) }});
    }
    newDocument.baseScore = recalculateBaseScore(newDocument);
    newDocument.score = recalculateScore(newDocument);
  }
  return newDocument;
}

/*

Cancel votes of a specific type on a given document (server)

*/
export const cancelVoteServer = async ({ document, voteType, collection, user, updateDocument }) => {

  const newDocument = _.clone(document);
  const vote = Votes.findOne({documentId: document._id, userId: user._id, voteType})
  // remove vote object
  await Connectors.delete(Votes, {_id: vote._id});
  newDocument.baseScore = recalculateBaseScore(newDocument);
  newDocument.score = recalculateScore(newDocument);

  if (updateDocument) {
    // update document score
    await Connectors.update(
      collection,
      {_id: document._id},
      {$set: {
        inactive: false,
        score: newDocument.score,
        baseScore: newDocument.baseScore
      }}
    );
  }
  return {newDocument, vote};
}

/*

Determine a user's voting power for a given operation.
If power is a function, call it on user

*/
const getVotePower = ({ user, voteType, document }) => {
  const power = voteTypes[voteType] && voteTypes[voteType].power || 1;
  return typeof power === 'function' ? power(user, document) : power;
};

/*

Create new vote object

*/
const createVote = ({ document, collectionName, voteType, user, voteId }) => {

  const vote = {
    documentId: document._id,
    collectionName,
    userId: user._id,
    voteType: voteType,
    power: getVotePower({user, voteType, document}),
    votedAt: new Date(),
    __typename: 'Vote'
  }

  // when creating a vote from the server, voteId can sometimes be undefined
  if (voteId) vote._id = voteId;

  return vote;

};

/*

Optimistic response for votes

*/
export const performVoteClient = ({ document, collection, voteType = 'upvote', user, voteId }) => {

  const collectionName = collection.options.collectionName;
  let returnedDocument;

  // console.log('// voteOptimisticResponse')
  // console.log('collectionName: ', collectionName)
  // console.log('document:', document)
  // console.log('voteType:', voteType)

  // make sure item and user are defined
  if (!document || !user || !Users.canDo(user, `${collectionName.toLowerCase()}.${voteType}`)) {
    throw new Error(`Cannot perform operation '${collectionName.toLowerCase()}.${voteType}'`);
  }

  let voteOptions = {document, collection, voteType, user, voteId};

  if (hasVotedClient({document, voteType})) {

    // console.log('action: cancel')
    returnedDocument = cancelVoteClient(voteOptions);
    returnedDocument = runCallbacks(`votes.cancel.client`, returnedDocument, collection, user);

  } else {

    // console.log('action: vote')

    if (voteTypes[voteType].exclusive) {
      returnedDocument = runCallbacks(`votes.clear.client`, voteOptions.document, collection, user);
      voteOptions.document = clearVotesClient({document:returnedDocument, collection, voteType, user, voteId})

    }
    returnedDocument = addVoteClient(voteOptions);
    returnedDocument = runCallbacks(`votes.${voteType}.client`, returnedDocument, collection, user, voteType);
  }

  // console.log('returnedDocument:', returnedDocument)

  return returnedDocument;
}

/*

Server-side database operation

### updateDocument
if set to true, this will perform its own database updates. If false, will only
return an updated document without performing any database operations on it.

*/
export const performVoteServer = async ({ documentId, document, voteType = 'bigUpvote', collection, voteId, user, updateDocument = true }) => {

  const collectionName = collection.options.collectionName;
  document = document || await Connectors.get(collection, documentId);

  debug('');
  debugGroup(`--------------- start \x1b[35mperformVoteServer\x1b[0m  ---------------`);
  debug('collectionName: ', collectionName);
  debug('document: ', document);
  debug('voteType: ', voteType);

  const voteOptions = {document, collection, voteType, user, voteId, updateDocument};

  if (!document || !user || !Users.canDo(user, `${collectionName.toLowerCase()}.${voteType}`)) {
    const VoteError = createError('voting.no_permission', {message: 'voting.no_permission'});
    throw new VoteError();
  }

  const existingVote = await hasVotedServer({document, voteType, user});

  if (existingVote) {

    // console.log('action: cancel')

    // runCallbacks(`votes.cancel.sync`, document, collection, user);
    let voteDocTuple = await cancelVoteServer(voteOptions);
    voteDocTuple = await runCallbacks(`votes.cancel.sync`, voteDocTuple, collection, user);
    document = voteDocTuple.newDocument;
    runCallbacksAsync(`votes.cancel.async`, voteDocTuple, collection, user);

  } else {

    // console.log('action: vote')

    if (voteTypes[voteType].exclusive) {
      document = await clearVotesServer(voteOptions)
    }

    let voteDocTuple = await addVoteServer({...voteOptions, document}); //Make sure to pass the new document to addVoteServer
    voteDocTuple = await runCallbacks(`votes.${voteType}.sync`, voteDocTuple, collection, user);
    document = voteDocTuple.newDocument;
    runCallbacksAsync(`votes.${voteType}.async`, voteDocTuple, collection, user);
  }

  debug('document after vote: ', document);
  debugGroupEnd();
  debug(`--------------- end \x1b[35m performVoteServer\x1b[0m ---------------`);
  debug('');

  // const newDocument = collection.findOne(documentId);
  document.__typename = collection.options.typeName;
  return document;

}
<|endoftext|>


<commit_before>var redis = require('redis');

function retryStrategy (options) {
  if (options.attempt > 5) {
      // End reconnecting with built in error
      return undefined;
  }
  // Add a bit of a delay
  return Math.min(options.attempt * 100, 3000);
}

/**
 * redisCacheModule constructor
 * @constructor
 * @param config: {
 *    type:                           {string | 'redis'}
 *    verbose:                        {boolean | false},
 *    expiration:                     {integer | 900},
 *    readOnly:                       {boolean | false},
 *    checkOnPreviousEmpty            {boolean | true},
 *    backgroundRefreshIntervalCheck  {boolean | true},
 *    backgroundRefreshInterval       {integer | 60000},
 *    backgroundRefreshMinTtl         {integer | 70000},
 *    redisData:                      {object},
 *    redisUrl:                       {string},
 *    redisEnv:                       {string}
 * }
 */
function redisCacheModule(config){
  var self = this;
  config = config || {};
  self.verbose = config.verbose || false;
  self.defaultExpiration = config.defaultExpiration || 900;
  self.readOnly = (typeof config.readOnly === 'boolean') ? config.readOnly : false;
  self.checkOnPreviousEmpty = (typeof config.checkOnPreviousEmpty === 'boolean') ? config.checkOnPreviousEmpty : true;
  self.backgroundRefreshIntervalCheck = (typeof config.backgroundRefreshIntervalCheck === 'boolean') ? config.backgroundRefreshIntervalCheck : true;
  self.backgroundRefreshInterval = config.backgroundRefreshInterval || 60000;
  self.backgroundRefreshMinTtl = config.backgroundRefreshMinTtl || 70000;
  self.logJsonParseFailures = config.logJsonParseFailures || false;
  var refreshKeys = {};
  var backgroundRefreshEnabled = false;

  var noop = function(){};

  /**
   * Instantates an Exception to be thrown
   * @param {string} name
   * @param {string} message
   * @return {Exception}
   */
  function Exception(name, message){
    this.name = name;
    this.message = message;
  }

  /**
   * Error logging logic
   * @param {boolean} isError
   * @param {string} message
   * @param {object} data
   */
  function log(isError, message, data){
    var indentifier = 'redisCacheModule: ';
    if(self.verbose || isError){
      if(data) {
        console.log(indentifier + message, data);
      } else {
        console.log(indentifier + message);
      }
    }
  }

  /**
   * Handle the refresh callback from the consumer, save the data to redis.
   *
   * @param {string} key The key used to save.
   * @param {Object} data refresh keys data.
   * @param {Error|null} err consumer callback failure.
   * @param {*} response The consumer response.
   */
  function handleRefreshResponse (key, data, err, response) {
    if(!err) {
      this.set(key, response, data.lifeSpan, data.refresh, noop);
    }
  }

  /**
   * Refreshes all keys that were set with a refresh function
   */
  function backgroundRefresh() {
    var keys = Object.keys(refreshKeys);
    keys.forEach(function(key) {
      var data = refreshKeys[key];
      if(data.expiration - Date.now() < this.backgroundRefreshMinTtl){
        data.refresh(key, handleRefreshResponse.bind(this, key, data));
      }
    }, self);
  }

  /**
   * Initialize background refresh
   */
  function backgroundRefreshInit(){
    if(!backgroundRefreshEnabled){
      backgroundRefreshEnabled = true;
      if(self.backgroundRefreshIntervalCheck){
        if(self.backgroundRefreshInterval > self.backgroundRefreshMinTtl){
          throw new Exception('BACKGROUND_REFRESH_INTERVAL_EXCEPTION', 'backgroundRefreshInterval cannot be greater than backgroundRefreshMinTtl.');
        }
      }
      setInterval(function(){
        backgroundRefresh();
      }, self.backgroundRefreshInterval);
    }
  }

  /**
   ******************************************* PUBLIC FUNCTIONS *******************************************
   */

  /**
   * Get the value associated with a given key
   * @param {string} key
   * @param {function} cb
   * @param {string} cleanKey
   */
  self.get = function(key, cb, cleanKey){
    if(arguments.length < 2){
      throw new Exception('INCORRECT_ARGUMENT_EXCEPTION', '.get() requires 2 arguments.');
    }
    log(false, 'get() called:', {key: key});
    try {
      var cacheKey = (cleanKey) ? cleanKey : key;
      log(false, 'Attempting to get key:', {key: cacheKey});
      self.db.get(cacheKey, function(err, result){
        try {
          result = JSON.parse(result);
        } catch (err) {
          if(self.logJsonParseFailures) {
            log(true, 'Error parsing JSON, err:', err);
          }
        }
        cb(err, result);
      });
    } catch (err) {
      cb({name: 'GetException', message: err}, null);
    }
  };

  /**
   * Get multiple values given multiple keys
   * @param {array} keys
   * @param {function} cb
   * @param {integer} index
   */
  self.mget = function(keys, cb, index){
    if(arguments.length < 2){
      throw new Exception('INCORRECT_ARGUMENT_EXCEPTION', '.mget() requires 2 arguments.');
    }
    log(false, '.mget() called:', {keys: keys});
    self.db.mget(keys, function (err, response){
      var obj = {};
      for(var i = 0; i < response.length; i++){
        if(response[i] !== null){
          try {
            response[i] = JSON.parse(response[i]);
          } catch (err) {
            if(self.logJsonParseFailures) {
              log(true, 'Error parsing JSON, err:', err);
            }
          }
          obj[keys[i]] = response[i];
        }
      }
      cb(err, obj, index);
    });
  };

  /**
   * Associate a key and value and optionally set an expiration
   * @param {string} key
   * @param {string | object} value
   * @param {integer} expiration
   * @param {function} refresh
   * @param {function} cb
   */
  self.set = function(){
    if(arguments.length < 2){
      throw new Exception('INCORRECT_ARGUMENT_EXCEPTION', '.set() requires a minimum of 2 arguments.');
    }
    var key = arguments[0];
    var value = arguments[1];
    var expiration = arguments[2] || null;
    var refresh = (arguments.length === 5) ? arguments[3] : null;
    var cb = (arguments.length === 5) ? arguments[4] : arguments[3];
    cb = cb || noop;
    log(false, '.set() called:', {key: key, value: value});
    try {
      if(!self.readOnly){
        expiration = expiration || self.defaultExpiration;
        var exp = (expiration * 1000) + Date.now();
        if(typeof value === 'object'){
          try {
            value = JSON.stringify(value);
          } catch (err) {
            if(self.logJsonParseFailures) {
              log(true, 'Error converting to JSON, err:', err);
            }
          }
        }
        if(refresh){
          self.db.set(key, value, 'nx', 'ex', expiration, function (err, response){

            refreshKeys[key] = {expiration: exp, lifeSpan: expiration, refresh: refresh};

            if(!err && response){
              cb(err, response);
              backgroundRefreshInit();
            }
            else{
              self.db.setex(key, expiration, value, cb);
            }
          });
        }
        else{
          self.db.setex(key, expiration, value, cb);
        }
      }
    }catch (err) {
      log(true, '.set() failed for cache of type ' + self.type, {name: 'RedisSetException', message: err});
    }
  };

  /**
   * Associate multiple keys with multiple values and optionally set expirations per function and/or key
   * @param {object} obj
   * @param {integer} expiration
   * @param {function} cb
   */
  self.mset = function(obj, expiration, cb){
    if(arguments.length < 1){
      throw new Exception('INCORRECT_ARGUMENT_EXCEPTION', '.mset() requires a minimum of 1 argument.');
    }
    log(false, '.mset() called:', {data: obj});
    var multi = self.db.multi();
    for(var key in obj){
      if(obj.hasOwnProperty(key)){
        var tempExpiration = expiration || self.defaultExpiration;
        var value = obj[key];
        if(typeof value === 'object' && value.cacheValue){
          tempExpiration = value.expiration || tempExpiration;
          value = value.cacheValue;
        }
        try {
          value = JSON.stringify(value);
        } catch (err) {
          if(self.logJsonParseFailures) {
            log(true, 'Error converting to JSON, err:', err);
          }
        }
        multi.setex(key, tempExpiration, value);
      }
    }
    multi.exec(function (err, replies){
      if(cb) {
        cb(err, replies);
      }
    });
  };

  /**
   * Delete the provided keys and their associated values
   * @param {array} keys
   * @param {function} cb
   */
  self.del = function(keys, cb){
    if(arguments.length < 1){
      throw new Exception('INCORRECT_ARGUMENT_EXCEPTION', '.del() requires a minimum of 1 argument.');
    }
    log(false, '.del() called:', {keys: keys});
    try {
      self.db.del(keys, function (err, count){
        if(cb){
          cb(err, count);
        }
      });
      if(typeof keys === 'object'){
        for(var i = 0; i < keys.length; i++){
          var key = keys[i];
          delete refreshKeys[key];
        }
      }
      else{
        delete refreshKeys[keys];
      }
    } catch (err) {
      log(true, '.del() failed for cache of type ' + self.type, err);
    }
  };

  /**
   * Flush all keys and values from all configured caches in cacheCollection
   * @param {function} cb
   */
  self.flush = function(cb){
    log(false, '.flush() called');
    try {
      self.db.flushall();
      refreshKeys = {};
    } catch (err) {
      log(true, '.flush() failed for cache of type ' + self.type, err);
    }
    if(cb) {
      cb();
    }
  };

  /**
   ******************************************* PRIVATE FUNCTIONS *******************************************
   */

  /**
   * Initialize redisCacheModule given the provided constructor params
   */
  function init(){
    self.type = config.type || 'redis';
    if(config.redisMock){
      self.db = config.redisMock;
    }
    else{
      if(config.redisUrl){
        self.redisData = config.redisUrl || null;
      }
      else if(config.redisEnv){
        self.redisData = process.env[config.redisEnv] || null;
      }
      else if(config.redisData){
        self.redisData = config.redisData;
      }
      self.readOnly = (typeof config.readOnly === 'boolean') ? config.readOnly : false;
      try {
        if (self.redisData) {
          if(typeof self.redisData === 'string'){
            self.db = redis.createClient(self.redisData,
              {'no_ready_check': true,
              retry_strategy: retryStrategy});
          } else {
            self.db = redis.createClient(self.redisData.port,
              self.redisData.hostname, {'no_ready_check': true,
              retry_strategy: retryStrategy});
            self.db.auth(self.redisData.auth);
          }
          self.db.on('error', function(err) {
            console.log('Error ' + err);
          });
          process.on('SIGTERM', function() {
            self.db.quit();
          });
          log(false, 'Redis client created with the following defaults:', {expiration: self.defaultExpiration, verbose: self.verbose, readOnly: self.readOnly});
        } else {
          self.db = false;
          log(false, 'Redis client not created: no redis config provided');
        }
      } catch (err) {
        self.db = false;
        log(true, 'Redis client not created:', err);
      }
    }
  }

  init();
}

module.exports = redisCacheModule;
redisCacheModule._retryStrategy = retryStrategy;<commit_msg>Check to see if auth information is passed and if so, forward it on to redis<commit_after>var redis = require('redis');

function retryStrategy (options) {
  if (options.attempt > 5) {
      // End reconnecting with built in error
      return undefined;
  }
  // Add a bit of a delay
  return Math.min(options.attempt * 100, 3000);
}

/**
 * redisCacheModule constructor
 * @constructor
 * @param config: {
 *    type:                           {string | 'redis'}
 *    verbose:                        {boolean | false},
 *    expiration:                     {integer | 900},
 *    readOnly:                       {boolean | false},
 *    checkOnPreviousEmpty            {boolean | true},
 *    backgroundRefreshIntervalCheck  {boolean | true},
 *    backgroundRefreshInterval       {integer | 60000},
 *    backgroundRefreshMinTtl         {integer | 70000},
 *    redisData:                      {object},
 *    redisUrl:                       {string},
 *    redisEnv:                       {string}
 * }
 */
function redisCacheModule(config){
  var self = this;
  config = config || {};
  self.verbose = config.verbose || false;
  self.defaultExpiration = config.defaultExpiration || 900;
  self.readOnly = (typeof config.readOnly === 'boolean') ? config.readOnly : false;
  self.checkOnPreviousEmpty = (typeof config.checkOnPreviousEmpty === 'boolean') ? config.checkOnPreviousEmpty : true;
  self.backgroundRefreshIntervalCheck = (typeof config.backgroundRefreshIntervalCheck === 'boolean') ? config.backgroundRefreshIntervalCheck : true;
  self.backgroundRefreshInterval = config.backgroundRefreshInterval || 60000;
  self.backgroundRefreshMinTtl = config.backgroundRefreshMinTtl || 70000;
  self.logJsonParseFailures = config.logJsonParseFailures || false;
  var refreshKeys = {};
  var backgroundRefreshEnabled = false;

  var noop = function(){};

  /**
   * Instantates an Exception to be thrown
   * @param {string} name
   * @param {string} message
   * @return {Exception}
   */
  function Exception(name, message){
    this.name = name;
    this.message = message;
  }

  /**
   * Error logging logic
   * @param {boolean} isError
   * @param {string} message
   * @param {object} data
   */
  function log(isError, message, data){
    var indentifier = 'redisCacheModule: ';
    if(self.verbose || isError){
      if(data) {
        console.log(indentifier + message, data);
      } else {
        console.log(indentifier + message);
      }
    }
  }

  /**
   * Handle the refresh callback from the consumer, save the data to redis.
   *
   * @param {string} key The key used to save.
   * @param {Object} data refresh keys data.
   * @param {Error|null} err consumer callback failure.
   * @param {*} response The consumer response.
   */
  function handleRefreshResponse (key, data, err, response) {
    if(!err) {
      this.set(key, response, data.lifeSpan, data.refresh, noop);
    }
  }

  /**
   * Refreshes all keys that were set with a refresh function
   */
  function backgroundRefresh() {
    var keys = Object.keys(refreshKeys);
    keys.forEach(function(key) {
      var data = refreshKeys[key];
      if(data.expiration - Date.now() < this.backgroundRefreshMinTtl){
        data.refresh(key, handleRefreshResponse.bind(this, key, data));
      }
    }, self);
  }

  /**
   * Initialize background refresh
   */
  function backgroundRefreshInit(){
    if(!backgroundRefreshEnabled){
      backgroundRefreshEnabled = true;
      if(self.backgroundRefreshIntervalCheck){
        if(self.backgroundRefreshInterval > self.backgroundRefreshMinTtl){
          throw new Exception('BACKGROUND_REFRESH_INTERVAL_EXCEPTION', 'backgroundRefreshInterval cannot be greater than backgroundRefreshMinTtl.');
        }
      }
      setInterval(function(){
        backgroundRefresh();
      }, self.backgroundRefreshInterval);
    }
  }

  /**
   ******************************************* PUBLIC FUNCTIONS *******************************************
   */

  /**
   * Get the value associated with a given key
   * @param {string} key
   * @param {function} cb
   * @param {string} cleanKey
   */
  self.get = function(key, cb, cleanKey){
    if(arguments.length < 2){
      throw new Exception('INCORRECT_ARGUMENT_EXCEPTION', '.get() requires 2 arguments.');
    }
    log(false, 'get() called:', {key: key});
    try {
      var cacheKey = (cleanKey) ? cleanKey : key;
      log(false, 'Attempting to get key:', {key: cacheKey});
      self.db.get(cacheKey, function(err, result){
        try {
          result = JSON.parse(result);
        } catch (err) {
          if(self.logJsonParseFailures) {
            log(true, 'Error parsing JSON, err:', err);
          }
        }
        cb(err, result);
      });
    } catch (err) {
      cb({name: 'GetException', message: err}, null);
    }
  };

  /**
   * Get multiple values given multiple keys
   * @param {array} keys
   * @param {function} cb
   * @param {integer} index
   */
  self.mget = function(keys, cb, index){
    if(arguments.length < 2){
      throw new Exception('INCORRECT_ARGUMENT_EXCEPTION', '.mget() requires 2 arguments.');
    }
    log(false, '.mget() called:', {keys: keys});
    self.db.mget(keys, function (err, response){
      var obj = {};
      for(var i = 0; i < response.length; i++){
        if(response[i] !== null){
          try {
            response[i] = JSON.parse(response[i]);
          } catch (err) {
            if(self.logJsonParseFailures) {
              log(true, 'Error parsing JSON, err:', err);
            }
          }
          obj[keys[i]] = response[i];
        }
      }
      cb(err, obj, index);
    });
  };

  /**
   * Associate a key and value and optionally set an expiration
   * @param {string} key
   * @param {string | object} value
   * @param {integer} expiration
   * @param {function} refresh
   * @param {function} cb
   */
  self.set = function(){
    if(arguments.length < 2){
      throw new Exception('INCORRECT_ARGUMENT_EXCEPTION', '.set() requires a minimum of 2 arguments.');
    }
    var key = arguments[0];
    var value = arguments[1];
    var expiration = arguments[2] || null;
    var refresh = (arguments.length === 5) ? arguments[3] : null;
    var cb = (arguments.length === 5) ? arguments[4] : arguments[3];
    cb = cb || noop;
    log(false, '.set() called:', {key: key, value: value});
    try {
      if(!self.readOnly){
        expiration = expiration || self.defaultExpiration;
        var exp = (expiration * 1000) + Date.now();
        if(typeof value === 'object'){
          try {
            value = JSON.stringify(value);
          } catch (err) {
            if(self.logJsonParseFailures) {
              log(true, 'Error converting to JSON, err:', err);
            }
          }
        }
        if(refresh){
          self.db.set(key, value, 'nx', 'ex', expiration, function (err, response){

            refreshKeys[key] = {expiration: exp, lifeSpan: expiration, refresh: refresh};

            if(!err && response){
              cb(err, response);
              backgroundRefreshInit();
            }
            else{
              self.db.setex(key, expiration, value, cb);
            }
          });
        }
        else{
          self.db.setex(key, expiration, value, cb);
        }
      }
    }catch (err) {
      log(true, '.set() failed for cache of type ' + self.type, {name: 'RedisSetException', message: err});
    }
  };

  /**
   * Associate multiple keys with multiple values and optionally set expirations per function and/or key
   * @param {object} obj
   * @param {integer} expiration
   * @param {function} cb
   */
  self.mset = function(obj, expiration, cb){
    if(arguments.length < 1){
      throw new Exception('INCORRECT_ARGUMENT_EXCEPTION', '.mset() requires a minimum of 1 argument.');
    }
    log(false, '.mset() called:', {data: obj});
    var multi = self.db.multi();
    for(var key in obj){
      if(obj.hasOwnProperty(key)){
        var tempExpiration = expiration || self.defaultExpiration;
        var value = obj[key];
        if(typeof value === 'object' && value.cacheValue){
          tempExpiration = value.expiration || tempExpiration;
          value = value.cacheValue;
        }
        try {
          value = JSON.stringify(value);
        } catch (err) {
          if(self.logJsonParseFailures) {
            log(true, 'Error converting to JSON, err:', err);
          }
        }
        multi.setex(key, tempExpiration, value);
      }
    }
    multi.exec(function (err, replies){
      if(cb) {
        cb(err, replies);
      }
    });
  };

  /**
   * Delete the provided keys and their associated values
   * @param {array} keys
   * @param {function} cb
   */
  self.del = function(keys, cb){
    if(arguments.length < 1){
      throw new Exception('INCORRECT_ARGUMENT_EXCEPTION', '.del() requires a minimum of 1 argument.');
    }
    log(false, '.del() called:', {keys: keys});
    try {
      self.db.del(keys, function (err, count){
        if(cb){
          cb(err, count);
        }
      });
      if(typeof keys === 'object'){
        for(var i = 0; i < keys.length; i++){
          var key = keys[i];
          delete refreshKeys[key];
        }
      }
      else{
        delete refreshKeys[keys];
      }
    } catch (err) {
      log(true, '.del() failed for cache of type ' + self.type, err);
    }
  };

  /**
   * Flush all keys and values from all configured caches in cacheCollection
   * @param {function} cb
   */
  self.flush = function(cb){
    log(false, '.flush() called');
    try {
      self.db.flushall();
      refreshKeys = {};
    } catch (err) {
      log(true, '.flush() failed for cache of type ' + self.type, err);
    }
    if(cb) {
      cb();
    }
  };

  /**
   ******************************************* PRIVATE FUNCTIONS *******************************************
   */

  /**
   * Initialize redisCacheModule given the provided constructor params
   */
  function init(){
    self.type = config.type || 'redis';
    if(config.redisMock){
      self.db = config.redisMock;
    }
    else{
      if(config.redisUrl){
        self.redisData = config.redisUrl || null;
      }
      else if(config.redisEnv){
        self.redisData = process.env[config.redisEnv] || null;
      }
      else if(config.redisData){
        self.redisData = config.redisData;
      }
      self.readOnly = (typeof config.readOnly === 'boolean') ? config.readOnly : false;
      try {
        if (self.redisData) {
          if(typeof self.redisData === 'string'){
            self.db = redis.createClient(self.redisData,
              {'no_ready_check': true,
              retry_strategy: retryStrategy});
          } else {
            self.db = redis.createClient(self.redisData.port,
              self.redisData.hostname, {'no_ready_check': true,
              retry_strategy: retryStrategy});

            // don't call redis auth method if no auth info passed
            if (self.redisData.auth != null) {
              self.db.auth(self.redisData.auth);
            }
          }
          self.db.on('error', function(err) {
            console.log('Error ' + err);
          });
          process.on('SIGTERM', function() {
            self.db.quit();
          });
          log(false, 'Redis client created with the following defaults:', {expiration: self.defaultExpiration, verbose: self.verbose, readOnly: self.readOnly});
        } else {
          self.db = false;
          log(false, 'Redis client not created: no redis config provided');
        }
      } catch (err) {
        self.db = false;
        log(true, 'Redis client not created:', err);
      }
    }
  }

  init();
}

module.exports = redisCacheModule;
redisCacheModule._retryStrategy = retryStrategy;<|endoftext|>


<commit_before>import layout from '../layout';
import i18n from '../../i18n';
import { header } from '../shared/common';
import { view as renderPromotion } from '../shared/offlineRound/promotion';
import helper from '../helper';
import { renderBoard } from '../round/view/roundView';
import menu, { renderUserInfos, renderSigninBox } from './menu';
import m from 'mithril';

export default function view(ctrl) {
  return layout.board(
    header.bind(undefined, i18n('training')),
    renderContent.bind(undefined, ctrl),
    () => [
      renderPromotion(ctrl),
      menu.view(ctrl.menu)
    ]
  );

}

function renderContent(ctrl) {
  if (!ctrl.data) return;

  if (helper.isPortrait())
    return [
      ctrl.data.mode === 'view' ? renderProblemDetails(ctrl) : renderExplanation(ctrl),
      renderBoard(ctrl),
      ctrl.data.mode === 'view' ? renderViewTable(ctrl) : renderPlayerTable(ctrl),
      renderActionsBar(ctrl)
    ];
  else
    return [
      renderBoard(ctrl),

          {ctrl.data.mode === 'view' ? renderProblemDetails(ctrl) : renderExplanation(ctrl)}

            { ctrl.data.user ? renderUserInfos(ctrl) : renderSigninBox()}

          {ctrl.data.mode === 'view' ? renderViewTable(ctrl) : renderPlayerTable(ctrl)}

        {renderActionsBar(ctrl)}

    ];
}

function renderExplanation(ctrl) {
  return (

        {i18n(ctrl.data.puzzle.color === 'white' ? 'findTheBestMoveForWhite' : 'findTheBestMoveForBlack')}

  );
}

function renderProblemDetails(ctrl) {

  const viewGame = ctrl.data.puzzle.gameId ? helper.ontouch(
    () => m.route(`/game/${ctrl.data.puzzle.gameId}/${ctrl.data.puzzle.color}`),
    () => window.plugins.toast.show(i18n('fromGameLink', ctrl.data.puzzle.gameId), 'short', 'bottom')
  ) : () => {};
  return (

        {i18n('puzzleId', ctrl.data.puzzle.id)}

{i18n('ratingX', ctrl.data.puzzle.rating)}

{i18n('playedXTimes', ctrl.data.puzzle.attempts)}

  );
}

function renderPlayerTable(ctrl) {
  return (

        {i18n(ctrl.chessground.data.turnColor === ctrl.data.puzzle.color ? 'yourTurn' : 'waiting')}

      {renderCommentary(ctrl)}
      {renderResult(ctrl)}

  );
}

function renderViewTable(ctrl) {
  return (

      {renderResult(ctrl)}

  );
}

function renderActionsBar(ctrl) {
  const vdom = [
    m('button.training_action.fa.fa-ellipsis-h', {
      key: 'puzzleMenu',
      config: helper.ontouch(ctrl.menu.open)
    }),
    m('button.training_action[data-icon=G]', {
      key: 'continueTraining',
      config: helper.ontouch(ctrl.newPuzzle, () => window.plugins.toast.show(i18n('continueTraining'), 'short', 'bottom'))
    })
  ];
  return m('section#training_actions', vdom.concat(
    ctrl.data.mode === 'view' ?
      renderViewControls(ctrl) :
      m('button.training_action[data-icon=b]', {
        key: 'giveUpPuzzle',
        config: helper.ontouch(ctrl.giveUp, () => window.plugins.toast.show(i18n('giveUp'), 'short', 'bottom'))
      })
  ).filter(el => el !== null));
}

function renderViewControls(ctrl) {
  var history = ctrl.data.replay.history;
  var step = ctrl.data.replay.step;
  return [
    m('button.training_action[data-icon=P]', {
      key: 'retryPuzzle',
      config: helper.ontouch(ctrl.retry, () => window.plugins.toast.show(i18n('retryThisPuzzle'), 'short', 'bottom'))
    }),
    m('button.training_action.fa.fa-share-alt', {
      key: 'sharePuzzle',
      config: helper.ontouch(ctrl.share, () => window.plugins.toast.show('Share this puzzle', 'short', 'bottom'))
    }),
    m('button.training_action[data-icon=I]', {
      config: helper.ontouch(ctrl.jumpPrev, ctrl.jumpFirst),
      key: 'historyPrev',
      className: helper.classSet({
        disabled: !(step !== step - 1 && step - 1 >= 0 && step - 1 < history.length)
      })
    }),
    m('button.training_action[data-icon=H]', {
      config: helper.ontouch(ctrl.jumpNext, ctrl.jumpLast),
      key: 'historyNext',
      className: helper.classSet({
        disabled: !(step !== step + 1 && step + 1 >= 0 && step + 1 < history.length)
      })
    })
  ];
}

function renderCommentary(ctrl) {
  switch (ctrl.data.comment) {
    case 'retry':
      return m('div.puzzleComment.retry', [
        m('h3.puzzleState', m('strong', i18n('goodMove'))),
        m('span', i18n('butYouCanDoBetter'))
      ]);
    case 'great':
      return m('div.puzzleComment.great', [
        m('h3.puzzleState.withIcon[data-icon=E]', m('strong', i18n('bestMove'))),
        m('span', i18n('keepGoing'))
      ]);
    case 'fail':
      return m('div.puzzleComment.fail', [
        m('h3.puzzleState.withIcon[data-icon=k]', m('strong', i18n('puzzleFailed'))),
        ctrl.data.mode === 'try' ? m('span', i18n('butYouCanKeepTrying')) : null
      ]);
    default:
      return ctrl.data.comment;
  }
}

function renderRatingDiff(diff) {
  return m('strong.puzzleRatingDiff', diff > 0 ? '+' + diff : diff);
}

function renderWin(ctrl, attempt) {
  return m('div.puzzleComment.win', [
    m('h3.puzzleState.withIcon[data-icon=E]', [
      m('strong', i18n('victory')),
      attempt ? renderRatingDiff(attempt.userRatingDiff) : null
    ]),
    attempt ? m('span', i18n('puzzleSolvedInXSeconds', attempt.seconds)) : null
  ]);
}

function renderLoss(ctrl, attempt) {
  return m('div.puzzleComment.loss',
    m('h3.puzzleState.withIcon[data-icon=k]', [
      m('strong', i18n('puzzleFailed')),
      attempt ? renderRatingDiff(attempt.userRatingDiff) : null
    ])
  );
}

function renderResult(ctrl) {
  switch (ctrl.data.win) {
    case true:
      return renderWin(ctrl, null);
    case false:
      return renderLoss(ctrl, null);
    default:
      switch (ctrl.data.attempt && ctrl.data.attempt.win) {
        case true:
          return renderWin(ctrl, ctrl.data.attempt);
        case false:
          return renderLoss(ctrl, ctrl.data.attempt);
      }
  }
}

<commit_msg>Play button only at end<commit_after>import layout from '../layout';
import i18n from '../../i18n';
import { header } from '../shared/common';
import { view as renderPromotion } from '../shared/offlineRound/promotion';
import helper from '../helper';
import { renderBoard } from '../round/view/roundView';
import menu, { renderUserInfos, renderSigninBox } from './menu';
import m from 'mithril';

export default function view(ctrl) {
  return layout.board(
    header.bind(undefined, i18n('training')),
    renderContent.bind(undefined, ctrl),
    () => [
      renderPromotion(ctrl),
      menu.view(ctrl.menu)
    ]
  );

}

function renderContent(ctrl) {
  if (!ctrl.data) return;

  if (helper.isPortrait())
    return [
      ctrl.data.mode === 'view' ? renderProblemDetails(ctrl) : renderExplanation(ctrl),
      renderBoard(ctrl),
      ctrl.data.mode === 'view' ? renderViewTable(ctrl) : renderPlayerTable(ctrl),
      renderActionsBar(ctrl)
    ];
  else
    return [
      renderBoard(ctrl),

          {ctrl.data.mode === 'view' ? renderProblemDetails(ctrl) : renderExplanation(ctrl)}

            { ctrl.data.user ? renderUserInfos(ctrl) : renderSigninBox()}

          {ctrl.data.mode === 'view' ? renderViewTable(ctrl) : renderPlayerTable(ctrl)}

        {renderActionsBar(ctrl)}

    ];
}

function renderExplanation(ctrl) {
  return (

        {i18n(ctrl.data.puzzle.color === 'white' ? 'findTheBestMoveForWhite' : 'findTheBestMoveForBlack')}

  );
}

function renderProblemDetails(ctrl) {

  const viewGame = ctrl.data.puzzle.gameId ? helper.ontouch(
    () => m.route(`/game/${ctrl.data.puzzle.gameId}/${ctrl.data.puzzle.color}`),
    () => window.plugins.toast.show(i18n('fromGameLink', ctrl.data.puzzle.gameId), 'short', 'bottom')
  ) : () => {};
  return (

        {i18n('puzzleId', ctrl.data.puzzle.id)}

{i18n('ratingX', ctrl.data.puzzle.rating)}

{i18n('playedXTimes', ctrl.data.puzzle.attempts)}

  );
}

function renderPlayerTable(ctrl) {
  return (

        {i18n(ctrl.chessground.data.turnColor === ctrl.data.puzzle.color ? 'yourTurn' : 'waiting')}

      {renderCommentary(ctrl)}
      {renderResult(ctrl)}

  );
}

function renderViewTable(ctrl) {
  return (

      {renderResult(ctrl)}

  );
}

function renderActionsBar(ctrl) {
  const vdom = [
    m('button.training_action.fa.fa-ellipsis-h', {
      key: 'puzzleMenu',
      config: helper.ontouch(ctrl.menu.open)
    })
  ];
  return m('section#training_actions', vdom.concat(
    ctrl.data.mode === 'view' ?
      renderViewControls(ctrl) :
      m('button.training_action[data-icon=b]', {
        key: 'giveUpPuzzle',
        config: helper.ontouch(ctrl.giveUp, () => window.plugins.toast.show(i18n('giveUp'), 'short', 'bottom'))
      })
  ).filter(el => el !== null));
}

function renderViewControls(ctrl) {
  var history = ctrl.data.replay.history;
  var step = ctrl.data.replay.step;
  return [
    m('button.training_action[data-icon=G]', {
      key: 'continueTraining',
      config: helper.ontouch(ctrl.newPuzzle, () => window.plugins.toast.show(i18n('continueTraining'), 'short', 'bottom'))
    }),
    m('button.training_action[data-icon=P]', {
      key: 'retryPuzzle',
      config: helper.ontouch(ctrl.retry, () => window.plugins.toast.show(i18n('retryThisPuzzle'), 'short', 'bottom'))
    }),
    m('button.training_action.fa.fa-share-alt', {
      key: 'sharePuzzle',
      config: helper.ontouch(ctrl.share, () => window.plugins.toast.show('Share this puzzle', 'short', 'bottom'))
    }),
    m('button.training_action[data-icon=I]', {
      config: helper.ontouch(ctrl.jumpPrev, ctrl.jumpFirst),
      key: 'historyPrev',
      className: helper.classSet({
        disabled: !(step !== step - 1 && step - 1 >= 0 && step - 1 < history.length)
      })
    }),
    m('button.training_action[data-icon=H]', {
      config: helper.ontouch(ctrl.jumpNext, ctrl.jumpLast),
      key: 'historyNext',
      className: helper.classSet({
        disabled: !(step !== step + 1 && step + 1 >= 0 && step + 1 < history.length)
      })
    })
  ];
}

function renderCommentary(ctrl) {
  switch (ctrl.data.comment) {
    case 'retry':
      return m('div.puzzleComment.retry', [
        m('h3.puzzleState', m('strong', i18n('goodMove'))),
        m('span', i18n('butYouCanDoBetter'))
      ]);
    case 'great':
      return m('div.puzzleComment.great', [
        m('h3.puzzleState.withIcon[data-icon=E]', m('strong', i18n('bestMove'))),
        m('span', i18n('keepGoing'))
      ]);
    case 'fail':
      return m('div.puzzleComment.fail', [
        m('h3.puzzleState.withIcon[data-icon=k]', m('strong', i18n('puzzleFailed'))),
        ctrl.data.mode === 'try' ? m('span', i18n('butYouCanKeepTrying')) : null
      ]);
    default:
      return ctrl.data.comment;
  }
}

function renderRatingDiff(diff) {
  return m('strong.puzzleRatingDiff', diff > 0 ? '+' + diff : diff);
}

function renderWin(ctrl, attempt) {
  return m('div.puzzleComment.win', [
    m('h3.puzzleState.withIcon[data-icon=E]', [
      m('strong', i18n('victory')),
      attempt ? renderRatingDiff(attempt.userRatingDiff) : null
    ]),
    attempt ? m('span', i18n('puzzleSolvedInXSeconds', attempt.seconds)) : null
  ]);
}

function renderLoss(ctrl, attempt) {
  return m('div.puzzleComment.loss',
    m('h3.puzzleState.withIcon[data-icon=k]', [
      m('strong', i18n('puzzleFailed')),
      attempt ? renderRatingDiff(attempt.userRatingDiff) : null
    ])
  );
}

function renderResult(ctrl) {
  switch (ctrl.data.win) {
    case true:
      return renderWin(ctrl, null);
    case false:
      return renderLoss(ctrl, null);
    default:
      switch (ctrl.data.attempt && ctrl.data.attempt.win) {
        case true:
          return renderWin(ctrl, ctrl.data.attempt);
        case false:
          return renderLoss(ctrl, ctrl.data.attempt);
      }
  }
}

<|endoftext|>


<commit_before>"use strict";

(function() {
  var projectKey = "projectkey";
  var todoKey = "todokey";
  var project = {key: projectKey};
  var author = angular.copy(window.currentUser);
  delete author.emails;
  var returnedtododata = {
    key: todoKey,
    author: author,
    title: "Test",
    content: {
      markdown: "yay todos!",
      html: "
yay todos!
\\n"
    },
    tags: [],
    due: null,
    done: false,
    assigned: null,
    milestone: null,
    date: 1388790824.0,
    children: []
  };

  var baseUrl = window.API_PREFIX + "/projects/" + projectKey + "/todos/";

  // Turn off jquery effects, which means animations returns instantly
  jQuery.fx.off = true;

  describe("TodoItemController", function() {
    var $httpBackend, controller, scope, TodoItem, TodoList, tododiv, $window;
    tododiv = $("

");
    tododiv.attr("id", "todo-" + todoKey);
    $("body").append(tododiv);

    $window = angular.copy(commonMocked.$window);

    beforeEach(angular.mock.module("projecto"));
    beforeEach(angular.mock.inject(function($rootScope, $controller, _$httpBackend_, Todos) {
      TodoItem = Todos.TodoItem;
      TodoList = Todos.TodoList;
      $httpBackend = _$httpBackend_;

      scope = $rootScope.$new();
      controller = $controller("TodoItemController", {
        $scope: scope,
        ProjectsService: commonMocked.ProjectsService,
        $window: $window
      });
      scope.currentProject = project;

      tododiv.css("display", "block");
    }));

    // As a working note. Right now a lot of infrastructure still depends on
    // jQuery. So therefore there will be a lot of hacks to hack around jquery
    // bits.

    it("should toggle todo", function() {
      var event = {
        preventDefault: function() { },
        stopPropagation: function() { }
      };

      var todo = new TodoItem(todoKey, project);
      scope.toggleTodo(todo, event);
      expect(tododiv.css("display")).toBe("none");

      scope.toggleTodo(todo, event);
      expect(tododiv.css("display")).toBe("block");
    });

    it("should mark todos as done", function() {
      var todo = new TodoItem(todoKey, project);

      scope.markDone(todo);
      $httpBackend.expectPOST(baseUrl + todoKey + "/markdone?archived=0", {done: true}).respond({status: "okay"});
      $httpBackend.flush();
      expect(todo.data.done).toBe(true);

      scope.markDone(todo);
      $httpBackend.expectPOST(baseUrl + todoKey + "/markdone?archived=0", {done: false}).respond({status: "okay"});
      $httpBackend.flush();
      expect(todo.data.done).toBe(false);
    });

    it("should start edit todos", function() {
      var todo = new TodoItem(todoKey, project);
      todo.data.title = "Test";
      spyOn(scope, "$emit");
      tododiv.css("display", "none");

      scope.editTodo(todo, 0);
      expect(tododiv.css("display")).toBe("block");
      expect(scope.todoDraft.data.title).toBe("Test");
      expect(scope.todoDraft._index).toBe(0);
      expect(scope.$emit).toHaveBeenCalledWith("enterEdit", todo.key, 0);

      scope.$emit.reset();
      // should cancel the edits
      scope.editTodo(todo, 0);
      expect(scope.$emit).toHaveBeenCalledWith("exitEdit", todo.key, 0);
      expect(scope.todoDraft).toBe(null);
    });

    it("should archive todos", function() {
      var todo = new TodoItem(todoKey, project);
      todo.data.title = "Test";
      spyOn(scope, "$emit");

      scope.archiveTodo(todo, 0);
      $httpBackend.expectDELETE(baseUrl + todoKey + "?archived=0&really=0").respond({status: "okay"});
      $httpBackend.flush();
      expect(scope.$emit).toHaveBeenCalledWith("archived", todo.key, 0);
      expect(todo.archived).toBe(true);
    });

    it("should delete todos", function() {
      var todo = new TodoItem(todoKey, project, {title: "Test"});
      spyOn(scope, "$emit");

      scope.deleteTodo(todo, 0);
      $httpBackend.expectDELETE(baseUrl + todoKey + "?archived=0&really=1").respond({status: "okay"});
      $httpBackend.flush();
      expect(scope.$emit).toHaveBeenCalledWith("deleted", todo.key, 0);

      scope.$emit.reset();
      todo.archived = true;
      scope.deleteTodo(todo, 0);
      $httpBackend.expectDELETE(baseUrl + todoKey + "?archived=1&really=1").respond({status: "okay"});
      $httpBackend.flush();
      expect(scope.$emit).toHaveBeenCalledWith("deleted", todo.key, 0);
    });

    it("should save todos", function() {
      var todo = new TodoItem(todoKey, project, {title: "Test", content: "yay todos!"});
      spyOn(scope, "$emit");

      scope.editTodo(todo, 0);
      scope.saveTodo(todo);
      $httpBackend.expectPUT(baseUrl + todoKey).respond(returnedtododata);
      $httpBackend.flush();
      expect(scope.$emit).toHaveBeenCalledWith("saved", returnedtododata, 0);
      expect(scope.$emit).toHaveBeenCalledWith("exitEdit", todo.key, 0);
      expect(scope.todoDraft).toBe(null);
    });

    it("should cancel edits", function() {
      var todo = new TodoItem(todoKey, project, {title: "Test", content: "yay todos!"});
      todo._index = 0;

      spyOn(scope, "$emit");
      spyOn($window, "confirm").andCallThrough();

      scope.todoDraft = todo;
      scope.cancelEdit(todo.key);
      expect(scope.todoDraft).toBe(null);
      expect(scope.$emit).toHaveBeenCalledWith("exitEdit", todo.key, 0);
      expect($window.confirm).toHaveBeenCalled();

      scope.$emit.reset();
      $window.confirm.reset();

      scope.todoDraft = todo;
      scope.cancelEdit(todo.key, true);
      expect($window.confirm).not.toHaveBeenCalled();
      expect(scope.todoDraft).toBe(null);
      expect(scope.$emit).toHaveBeenCalledWith("exitEdit", todo.key, 0);
    });

  });

  describe("TodosController", function() {
    var $httpBackend, controller, scope, TodoItem, TodoList, $window, rootScope, newcontroller, new_todo_box;

    beforeEach(angular.mock.module("projecto"));
    beforeEach(angular.mock.inject(function($rootScope, $controller, _$httpBackend_, Todos) {
      TodoItem = Todos.TodoItem;
      TodoList = Todos.TodoList;
      $httpBackend = _$httpBackend_;
      rootScope = $rootScope;
      newcontroller = $controller;

      scope = $rootScope.$new();
      controller = $controller("TodosController", {
        $scope: scope,
        $window: commonMocked.$window,
        ProjectsService: commonMocked.ProjectsService
      });
      scope.currentProject = project;
      scope.todolist = new TodoList(project);

      new_todo_box = $("

");
      new_todo_box.css("display", "none");
      $("body").append(new_todo_box);

    }));

    afterEach(function() {
      new_todo_box.remove();
      new_todo_box = null;
    });

    it("should subscribe to signals", function() {
      var scope = rootScope.$new();
      spyOn(scope, "$on");

      var controller = newcontroller("TodosController", {
        $scope: scope,
        $window: commonMocked.$window,
        ProjectsService: commonMocked.ProjectsService
      });

      expect(scope.$on).toHaveBeenCalledWith("saved", jasmine.any(Function));
      expect(scope.$on).toHaveBeenCalledWith("archived", jasmine.any(Function));
      expect(scope.$on).toHaveBeenCalledWith("deleted", jasmine.any(Function));
      expect(scope.$on).toHaveBeenCalledWith("enterEdit", jasmine.any(Function));
      expect(scope.$on).toHaveBeenCalledWith("exitEdit", jasmine.any(Function));
    });

    it("should expand and close new todo box", function() {
      scope.new_todo();
      expect(new_todo_box.css("display")).toBe("block");
      expect(scope.newtodo).not.toBe(null);
      expect(scope.newtodo instanceof TodoItem).toBe(true);
      expect(scope.newtodo.project).toBe(project);

      scope.cancel_new_todo();
      expect(new_todo_box.css("display")).toBe("none");
      expect(scope.newtodo).toBe(null);
    });

    it("should create a todo", function() {
      scope.new_todo();
      scope.newtodo.title = "Test";
      scope.newtodo.content = "yay todos!";
      spyOn(scope.newtodo, "save").andCallThrough();

      scope.create_todo();
      expect(scope.newtodo.save).toHaveBeenCalled();

      var newtodo = scope.newtodo;
      $httpBackend.expectPOST(baseUrl).respond(returnedtododata);
      $httpBackend.flush();

      expect(scope.todolist.todos.length).toBe(1);
      expect(scope.todolist.todos[0]).toBe(newtodo);
      expect(scope.newtodo).toEqual(null);
    });

    it("should clear done", function() {
      scope.todolist = new TodoList(project);

      var list = [];
      for (var i=0; i<10; i++) {
        list.push(angular.copy(returnedtododata));
        list[i].key = list[i].key + i;
        if (i % 3 === 0) {
          list[i].done = true;
        }
      }

      scope.todolist.fetch();
      $httpBackend.expectGET(baseUrl + "filter?page=1&showdone=0&shownotdone=1").respond({
        todos: list,
        currentPage: 1,
        totalTodos: 10,
        todosPerPage: 20
      });
      $httpBackend.flush();

    });

    it("should update", function() {

    });

    it("should toggle filter tags", function() {

    });

    it("should toggle show done", function() {

    });
  });
})();
<commit_msg>Added the rest of the tests<commit_after>"use strict";

(function() {
  var projectKey = "projectkey";
  var todoKey = "todokey";
  var project = {key: projectKey};
  var author = angular.copy(window.currentUser);
  delete author.emails;
  var returnedtododata = {
    key: todoKey,
    author: author,
    title: "Test",
    content: {
      markdown: "yay todos!",
      html: "
yay todos!
\\n"
    },
    tags: [],
    due: null,
    done: false,
    assigned: null,
    milestone: null,
    date: 1388790824.0,
    children: []
  };

  var baseUrl = window.API_PREFIX + "/projects/" + projectKey + "/todos/";

  // Turn off jquery effects, which means animations returns instantly
  jQuery.fx.off = true;

  describe("TodoItemController", function() {
    var $httpBackend, controller, scope, TodoItem, TodoList, tododiv, $window;
    tododiv = $("

");
    tododiv.attr("id", "todo-" + todoKey);
    $("body").append(tododiv);

    $window = angular.copy(commonMocked.$window);

    beforeEach(angular.mock.module("projecto"));
    beforeEach(angular.mock.inject(function($rootScope, $controller, _$httpBackend_, Todos) {
      TodoItem = Todos.TodoItem;
      TodoList = Todos.TodoList;
      $httpBackend = _$httpBackend_;

      scope = $rootScope.$new();
      controller = $controller("TodoItemController", {
        $scope: scope,
        ProjectsService: commonMocked.ProjectsService,
        $window: $window
      });
      scope.currentProject = project;

      tododiv.css("display", "block");
    }));

    // As a working note. Right now a lot of infrastructure still depends on
    // jQuery. So therefore there will be a lot of hacks to hack around jquery
    // bits.

    it("should toggle todo", function() {
      var event = {
        preventDefault: function() { },
        stopPropagation: function() { }
      };

      var todo = new TodoItem(todoKey, project);
      scope.toggleTodo(todo, event);
      expect(tododiv.css("display")).toBe("none");

      scope.toggleTodo(todo, event);
      expect(tododiv.css("display")).toBe("block");
    });

    it("should mark todos as done", function() {
      var todo = new TodoItem(todoKey, project);

      scope.markDone(todo);
      $httpBackend.expectPOST(baseUrl + todoKey + "/markdone?archived=0", {done: true}).respond({status: "okay"});
      $httpBackend.flush();
      expect(todo.data.done).toBe(true);

      scope.markDone(todo);
      $httpBackend.expectPOST(baseUrl + todoKey + "/markdone?archived=0", {done: false}).respond({status: "okay"});
      $httpBackend.flush();
      expect(todo.data.done).toBe(false);
    });

    it("should start edit todos", function() {
      var todo = new TodoItem(todoKey, project);
      todo.data.title = "Test";
      spyOn(scope, "$emit");
      tododiv.css("display", "none");

      scope.editTodo(todo, 0);
      expect(tododiv.css("display")).toBe("block");
      expect(scope.todoDraft.data.title).toBe("Test");
      expect(scope.todoDraft._index).toBe(0);
      expect(scope.$emit).toHaveBeenCalledWith("enterEdit", todo.key, 0);

      scope.$emit.reset();
      // should cancel the edits
      scope.editTodo(todo, 0);
      expect(scope.$emit).toHaveBeenCalledWith("exitEdit", todo.key, 0);
      expect(scope.todoDraft).toBe(null);
    });

    it("should archive todos", function() {
      var todo = new TodoItem(todoKey, project);
      todo.data.title = "Test";
      spyOn(scope, "$emit");

      scope.archiveTodo(todo, 0);
      $httpBackend.expectDELETE(baseUrl + todoKey + "?archived=0&really=0").respond({status: "okay"});
      $httpBackend.flush();
      expect(scope.$emit).toHaveBeenCalledWith("archived", todo.key, 0);
      expect(todo.archived).toBe(true);
    });

    it("should delete todos", function() {
      var todo = new TodoItem(todoKey, project, {title: "Test"});
      spyOn(scope, "$emit");

      scope.deleteTodo(todo, 0);
      $httpBackend.expectDELETE(baseUrl + todoKey + "?archived=0&really=1").respond({status: "okay"});
      $httpBackend.flush();
      expect(scope.$emit).toHaveBeenCalledWith("deleted", todo.key, 0);

      scope.$emit.reset();
      todo.archived = true;
      scope.deleteTodo(todo, 0);
      $httpBackend.expectDELETE(baseUrl + todoKey + "?archived=1&really=1").respond({status: "okay"});
      $httpBackend.flush();
      expect(scope.$emit).toHaveBeenCalledWith("deleted", todo.key, 0);
    });

    it("should save todos", function() {
      var todo = new TodoItem(todoKey, project, {title: "Test", content: "yay todos!"});
      spyOn(scope, "$emit");

      scope.editTodo(todo, 0);
      scope.saveTodo(todo);
      $httpBackend.expectPUT(baseUrl + todoKey).respond(returnedtododata);
      $httpBackend.flush();
      expect(scope.$emit).toHaveBeenCalledWith("saved", returnedtododata, 0);
      expect(scope.$emit).toHaveBeenCalledWith("exitEdit", todo.key, 0);
      expect(scope.todoDraft).toBe(null);
    });

    it("should cancel edits", function() {
      var todo = new TodoItem(todoKey, project, {title: "Test", content: "yay todos!"});
      todo._index = 0;

      spyOn(scope, "$emit");
      spyOn($window, "confirm").andCallThrough();

      scope.todoDraft = todo;
      scope.cancelEdit(todo.key);
      expect(scope.todoDraft).toBe(null);
      expect(scope.$emit).toHaveBeenCalledWith("exitEdit", todo.key, 0);
      expect($window.confirm).toHaveBeenCalled();

      scope.$emit.reset();
      $window.confirm.reset();

      scope.todoDraft = todo;
      scope.cancelEdit(todo.key, true);
      expect($window.confirm).not.toHaveBeenCalled();
      expect(scope.todoDraft).toBe(null);
      expect(scope.$emit).toHaveBeenCalledWith("exitEdit", todo.key, 0);
    });

  });

  describe("TodosController", function() {
    var $httpBackend, controller, scope, TodoItem, TodoList, $window, rootScope, newcontroller, new_todo_box;

    beforeEach(angular.mock.module("projecto"));
    beforeEach(angular.mock.inject(function($rootScope, $controller, _$httpBackend_, Todos) {
      TodoItem = Todos.TodoItem;
      TodoList = Todos.TodoList;
      $httpBackend = _$httpBackend_;
      rootScope = $rootScope;
      newcontroller = $controller;

      scope = $rootScope.$new();
      controller = $controller("TodosController", {
        $scope: scope,
        $window: commonMocked.$window,
        ProjectsService: commonMocked.ProjectsService
      });
      scope.currentProject = project;
      scope.todolist = new TodoList(project);

      new_todo_box = $("

");
      new_todo_box.css("display", "none");
      $("body").append(new_todo_box);

    }));

    afterEach(function() {
      new_todo_box.remove();
      new_todo_box = null;
    });

    it("should subscribe to signals", function() {
      var scope = rootScope.$new();
      spyOn(scope, "$on");

      var controller = newcontroller("TodosController", {
        $scope: scope,
        $window: commonMocked.$window,
        ProjectsService: commonMocked.ProjectsService
      });

      expect(scope.$on).toHaveBeenCalledWith("saved", jasmine.any(Function));
      expect(scope.$on).toHaveBeenCalledWith("archived", jasmine.any(Function));
      expect(scope.$on).toHaveBeenCalledWith("deleted", jasmine.any(Function));
      expect(scope.$on).toHaveBeenCalledWith("enterEdit", jasmine.any(Function));
      expect(scope.$on).toHaveBeenCalledWith("exitEdit", jasmine.any(Function));
    });

    it("should expand and close new todo box", function() {
      scope.new_todo();
      expect(new_todo_box.css("display")).toBe("block");
      expect(scope.newtodo).not.toBe(null);
      expect(scope.newtodo instanceof TodoItem).toBe(true);
      expect(scope.newtodo.project).toBe(project);

      scope.cancel_new_todo();
      expect(new_todo_box.css("display")).toBe("none");
      expect(scope.newtodo).toBe(null);
    });

    it("should create a todo", function() {
      scope.new_todo();
      scope.newtodo.title = "Test";
      scope.newtodo.content = "yay todos!";
      spyOn(scope.newtodo, "save").andCallThrough();

      scope.create_todo();
      expect(scope.newtodo.save).toHaveBeenCalled();

      var newtodo = scope.newtodo;
      $httpBackend.expectPOST(baseUrl).respond(returnedtododata);
      $httpBackend.flush();

      expect(scope.todolist.todos.length).toBe(1);
      expect(scope.todolist.todos[0]).toBe(newtodo);
      expect(scope.newtodo).toEqual(null);
    });

    it("should clear done", function() {
      scope.todolist = new TodoList(project);

      var list = [];
      for (var i=0; i<10; i++) {
        list.push(angular.copy(returnedtododata));
        list[i].key = list[i].key + i;
        if (i % 3 === 0) {
          list[i].done = true;
        }
      }

      scope.todolist.fetch();
      $httpBackend.expectGET(baseUrl + "filter?page=1&showdone=0&shownotdone=1").respond({
        todos: list,
        currentPage: 1,
        totalTodos: 10,
        todosPerPage: 20
      });
      $httpBackend.flush();

    });

    it("should update", function() {
      scope.todolist = new TodoList(project);
      spyOn(scope.todolist, "fetch").andCallThrough();
      scope.update(true);

      expect(scope.todolist.fetch).toHaveBeenCalledWith(true);

      $httpBackend.expectGET(baseUrl + "tags?archived=0").respond({
        tags: ["tag1", "tag2"]
      });

      var list = [];
      for (var i=0; i<10; i++) {
        list.push(angular.copy(returnedtododata));
        list[i].tags = ["tag1"];
        if (i % 3 == 0)
          list[i].tags = ["tag2"];

        list[i].key = list[i].key + i;
      }

      $httpBackend.expectGET(baseUrl + "filter?page=1&showdone=0&shownotdone=1&tags=tag1&tags=tag2").respond({
        todos: list,
        currentPage: 1,
        totalTodos: list.length,
        todosPerPage: 20
      });
      $httpBackend.flush();

      expect(scope.todolist.todos.length).toBe(list.length);
    });

    it("should toggle filter tags", function() {
      scope.todolist = new TodoList(project);
      scope.todolist.fetch(true);

      $httpBackend.expectGET(baseUrl + "tags?archived=0").respond({
        tags: ["tag1", "tag2"]
      });
      $httpBackend.expectGET(baseUrl + "filter?page=1&showdone=0&shownotdone=1&tags=tag1&tags=tag2").respond({
        todos: [],
        currentPage: 1,
        totalTodos: 0,
        todosPerPage: 20
      });
      $httpBackend.flush();

      expect(scope.todolist.isTagFiltered("tag1")).toBe(true);
      expect(scope.todolist.isTagFiltered("tag2")).toBe(true);

      spyOn(scope, "update");

      scope.toggle_filter_tag("tag1");
      expect(scope.todolist.isTagFiltered("tag1")).toBe(false);
      expect(scope.todolist.isTagFiltered("tag2")).toBe(true);
      expect(scope.update).toHaveBeenCalled();

      scope.update.reset();
      scope.toggle_filter_tag("tag1");
      expect(scope.todolist.isTagFiltered("tag1")).toBe(true);
      expect(scope.todolist.isTagFiltered("tag2")).toBe(true);
      expect(scope.update).toHaveBeenCalled();

      scope.update.reset();
      scope.toggle_filter_tag("tag2");
      expect(scope.todolist.isTagFiltered("tag1")).toBe(true);
      expect(scope.todolist.isTagFiltered("tag2")).toBe(false);
      expect(scope.update).toHaveBeenCalled();

      scope.update.reset();
      scope.toggle_filter_tag("tag2");
      expect(scope.todolist.isTagFiltered("tag1")).toBe(true);
      expect(scope.todolist.isTagFiltered("tag2")).toBe(true);
      expect(scope.update).toHaveBeenCalled();
    });

    it("should toggle show done and show not done", function() {
      scope.todolist = new TodoList(project);
      scope.todolist.fetch(true);

      $httpBackend.expectGET(baseUrl + "tags?archived=0").respond({
        tags: ["tag1", "tag2"]
      });
      $httpBackend.expectGET(baseUrl + "filter?page=1&showdone=0&shownotdone=1&tags=tag1&tags=tag2").respond({
        todos: [],
        currentPage: 1,
        totalTodos: 0,
        todosPerPage: 20
      });
      $httpBackend.flush();

      expect(scope.todolist.showdone).toBe(false);
      expect(scope.todolist.shownotdone).toBe(true);

      spyOn(scope, "update");
      scope.toggle_show_done();
      expect(scope.todolist.showdone).toBe(true);
      expect(scope.update).toHaveBeenCalled();

      scope.update.reset();
      scope.toggle_show_done();
      expect(scope.todolist.showdone).toBe(false);
      expect(scope.update).toHaveBeenCalled();

      scope.update.reset();
      scope.toggle_show_notdone();
      expect(scope.todolist.shownotdone).toBe(false);
      expect(scope.update).toHaveBeenCalled();

      scope.update.reset();
      scope.toggle_show_notdone();
      expect(scope.todolist.shownotdone).toBe(true);
      expect(scope.update).toHaveBeenCalled();
    });
  });
})();
<|endoftext|>


<commit_before>"use strict"

angular.module("schoolines").directive("deadline", function() {
    return {
        restrict: "AE",
        templateUrl: "/app/directives/deadline.html",
        controller: ["$mdSidenav","$location", "$scope", "$timeout", "$log", "AuthService", "IVLEService", "Session", "DeadlineService", "$localStorage",
        function($mdSidenav, $location, $scope, $timeout, $log, AuthService, IVLEService, Session, DeadlineService, $localStorage) {
            AuthService.autologin().then(function() {

                IVLEService.getModules(Session.token).then(function() {
                    $scope.modules = ["All", "Hidden"];
                    $scope.modules = $scope.modules.concat($localStorage.modules);
                    DeadlineService.getDeadline().then(function(){

                        var deadlines = JSON.parse($localStorage.deadlines.deadlineArray).filter(function(deadline){
                            if(!$localStorage.hiddenDeadlines) return true;
                            return !$localStorage.hiddenDeadlines.includes(deadline.id);
                        });
                        $scope.deadlines = deadlines;

                        $scope.filter = function(mod){
                            if(mod == "All"){
                                $scope.deadlines = deadlines;
                                $scope.close();
                                return ;
                            }

                            if(mod == "Hidden"){
                                $scope.deadlines = JSON.parse($localStorage.deadlines.deadlineArray).filter(function(deadline){
                                    return $localStorage.hiddenDeadlines.includes(deadline.id);

                                });
                                $scope.close();
                                return ;
                            }

                            var d = [];
                            for(var deadline of deadlines){
                                if(deadline.module == mod)

                                    d.push(deadline);
                            }
                            $scope.deadlines = d;
                            $scope.close();
                        }

                        for (var d of $scope.deadlines) {
                            // TODO change color

                            d.color = "red";
                        }
                    });

                });
            });

            $scope.view = function(deadline){
                DeadlineService.deadlineDetail = deadline;
                $location.path('/deadlineDetail');
            }

            $scope.close = function() {
                // Component lookup should always be available since we are not using `ng-if`
                $mdSidenav('left').close()
                    .then(function() {
                        $log.debug("close LEFT is done");
                    });

            };

            $scope.toggleLeft = buildDelayedToggler('left');
            /**
             * Supplies a function that will continue to operate until the
             * time is up.
             */
            function debounce(func, wait, context) {
                var timer;

                return function debounced() {
                    var context = $scope,
                        args = Array.prototype.slice.call(arguments);
                    $timeout.cancel(timer);
                    timer = $timeout(function() {
                        timer = undefined;
                        func.apply(context, args);
                    }, wait || 10);
                };
            }

            /**
             * Build handler to open/close a SideNav; when animation finishes
             * report completion in console
             */
            function buildDelayedToggler(navID) {
                return debounce(function() {
                    // Component lookup should always be available since we are not using `ng-if`
                    $mdSidenav(navID)
                        .toggle()
                        .then(function() {
                            $log.debug("toggle " + navID + " is done");
                        });
                }, 200);
            }

            function buildToggler(navID) {
                return function() {
                    // Component lookup should always be available since we are not using `ng-if`
                    $mdSidenav(navID)
                        .toggle()
                        .then(function() {
                            $log.debug("toggle " + navID + " is done");
                        });
                }
            }

        }]
    }
});
<commit_msg>display same deadlines when back from detail<commit_after>"use strict"

angular.module("schoolines").directive("deadline", function() {
    return {
        restrict: "AE",
        templateUrl: "/app/directives/deadline.html",
        controller: ["$mdSidenav","$location", "$scope", "$timeout", "$log", "AuthService", "IVLEService", "Session", "DeadlineService", "$localStorage",
        function($mdSidenav, $location, $scope, $timeout, $log, AuthService, IVLEService, Session, DeadlineService, $localStorage) {
            AuthService.autologin().then(function() {

                IVLEService.getModules(Session.token).then(function() {
                    $scope.modules = ["All", "Hidden"];
                    $scope.modules = $scope.modules.concat($localStorage.modules);
                    DeadlineService.getDeadline().then(function(){

                        var deadlines = JSON.parse($localStorage.deadlines.deadlineArray).filter(function(deadline){
                            if(!$localStorage.hiddenDeadlines) return true;
                            return !$localStorage.hiddenDeadlines.includes(deadline.id);
                        });
                        $scope.deadlines = deadlines;

                        $scope.filter = function(mod){
                            DeadlineService.currentMod = mod;
                            if(mod == "All"){
                                $scope.deadlines = deadlines;
                                $scope.close();
                                return ;
                            }

                            if(mod == "Hidden"){
                                $scope.deadlines = JSON.parse($localStorage.deadlines.deadlineArray).filter(function(deadline){
                                    return $localStorage.hiddenDeadlines.includes(deadline.id);

                                });
                                $scope.close();
                                return ;
                            }

                            var d = [];
                            for(var deadline of deadlines){
                                if(deadline.module == mod)

                                    d.push(deadline);
                            }
                            $scope.deadlines = d;
                            $scope.close();
                        }

                        $scope.filter(DeadlineService.currentMod);

                        for (var d of $scope.deadlines) {
                            // TODO change color

                            d.color = "red";
                        }
                    });

                });
            });

            $scope.view = function(deadline){
                DeadlineService.deadlineDetail = deadline;
                $location.path('/deadlineDetail');
            }

            $scope.close = function() {
                // Component lookup should always be available since we are not using `ng-if`
                $mdSidenav('left').close()
                    .then(function() {
                        $log.debug("close LEFT is done");
                    });

            };

            $scope.toggleLeft = buildDelayedToggler('left');
            /**
             * Supplies a function that will continue to operate until the
             * time is up.
             */
            function debounce(func, wait, context) {
                var timer;

                return function debounced() {
                    var context = $scope,
                        args = Array.prototype.slice.call(arguments);
                    $timeout.cancel(timer);
                    timer = $timeout(function() {
                        timer = undefined;
                        func.apply(context, args);
                    }, wait || 10);
                };
            }

            /**
             * Build handler to open/close a SideNav; when animation finishes
             * report completion in console
             */
            function buildDelayedToggler(navID) {
                return debounce(function() {
                    // Component lookup should always be available since we are not using `ng-if`
                    $mdSidenav(navID)
                        .toggle()
                        .then(function() {
                            $log.debug("toggle " + navID + " is done");
                        });
                }, 200);
            }

            function buildToggler(navID) {
                return function() {
                    // Component lookup should always be available since we are not using `ng-if`
                    $mdSidenav(navID)
                        .toggle()
                        .then(function() {
                            $log.debug("toggle " + navID + " is done");
                        });
                }
            }

        }]
    }
});
<|endoftext|>