<commit_before>'use strict';

/* global asyncStorage, SumoDB, Utils, User, nunjucks */

(function(exports) {
  var question_thread;
  var question_id;
  var dialog;
  var user;

  /**
   * Handles close button events from a fialog modal.
   */
  function dialog_handler() {
    dialog = document.querySelector('#first_question_help');
    var closeButton = dialog.querySelector('#confirm');

    closeButton.addEventListener('click', function(evt) {
      evt.preventDefault();
      dialog.classList.add('hide');
    });
  }

  /**
   * Adds the thread header that displays, the relative time the question was
   * posted, the device (if already categorised) and the display name of the
   * user who posted the question.
   * @param {object} question - The question JSON object
   */
  function add_thread_header(question) {
    var handset_type;
    var date_posted = Utils.time_since(new Date(question.updated));
    var author = question.creator.display_name || question.creator.username;

    for (var i = 0, l = question.metadata.length; i < l; i++) {
      var current_item = question.metadata[i];
      if (current_item.name === 'device_type') {
        handset_type = current_item.value;
        break;
      }
    }
    var html = nunjucks.render('thread_header.html', {
      date_posted: date_posted,
      handset_type: handset_type,
      author: author
    });
    question_thread.insertAdjacentHTML('afterbegin', html);
  }

  /**
   * Adds the footer section to the question thread. Currently this consist
   * of the receive new notifications settings chckbox.
   */
  function add_thread_footer() {
    var do_notify = user.settings[0].value === 'True' ? true : false;
    var html = nunjucks.render('thread_footer.html', {
      new_comment_notify: do_notify
    });
    question_thread.insertAdjacentHTML('beforeend', html);
  }

  function submit_comment(evt) {
    evt.preventDefault();
    var comment = document.getElementById('question_field').value;

    Utils.toggle_spinner();

    document.getElementById('thread-introduction').classList.add('hide');
    document.getElementById('question-thread').classList.remove('hide');

    var fake_comment = nunjucks.render('comment.html',
      {comment: {content: comment}});
    var list = document.getElementById('comment-list');
    var list_item = document.createElement('li');

    list.appendChild(list_item).innerHTML += fake_comment;
    list_item.scrollIntoView();

    var submit_promise;
    if (question_id) {
      submit_promise = submit_answer(question_id, comment);
    } else {
      submit_promise = submit_question(comment).then(function(comment) {

        add_thread_header(comment);
        add_thread_footer();

        comment.content = comment.title;
        return comment;
      });
    }

    submit_promise.then(function(comment) {

      Utils.toggle_spinner();

      // Only handle first time help message scenario for questions
      if (comment.answers) {
        asyncStorage.getItem('seen_first_question_help', function(response) {
          // See if the flag exist
          if (!response) {
            // flag does not exist, show the dialog and set the flag
            dialog.classList.remove('hide');
            asyncStorage.setItem('seen_first_question_help', true);
          }
        });
      }

      comment.created = Utils.time_since(new Date(comment.created));
      list_item.innerHTML = nunjucks.render('comment.html',
        {comment: comment});
      window.top.postMessage({question_id: question_id, comment: comment}, '*');
    });
  }

  function submit_question(comment) {
    return SumoDB.post_question(comment).then(function(response) {
      question_id = response.id;
      return response;
    });
  }

  function submit_answer(question_id, comment) {
    return SumoDB.post_answer(question_id, comment).then(function(response) {
      return response;
    });
  }

  /**
   * Adds an event listener to the new comment notify checkbox and updates
   * the current user's preference on change events.
   */
  function pref_change_listener() {
    var new_comments_notification = document.querySelector('#new_comments_notification');

    // only add event listener if the element exists
    if (new_comments_notification) {
      new_comments_notification.addEventListener('change', function() {
        User.set_preference('new_comment_notify', this.checked);
      });
    }
  }

  function show_question() {
    if (!question_id) {
      Utils.toggle_spinner();
      return;
    }

    document.getElementById('question-thread').classList.remove('hide');

    var question_content = [];
    question_content.push(SumoDB.get_question(question_id));

    question_content.push(SumoDB.get_answers_for_question(question_id));

    Promise.all(question_content).then(function([question, answers]) {

      Utils.toggle_spinner();

      question.content = question.title;
      answers.push(question);
      answers.reverse();

      for (var i = 0, l = answers.length; i < l; i++) {
        var created = answers[i].created;
        answers[i].created = Utils.time_since(new Date(created));
      }

      add_thread_header(question);
      var html = nunjucks.render('thread.html', {
        user: user,
        results: answers
      });
      var list = document.getElementById('comment-list');
      list.insertAdjacentHTML('beforeend', html);
      add_thread_footer();

      // listen for clicks on new comment notify checkbox
      pref_change_listener();
    });
  }

  var QuestionController = {
    init: function() {
      var question_view = location.search ? true : false;

      // if this is a question view, not arrived at from clicking/tapping
      // ask a question, hide the introduction section.
      if (question_view) {
        document.getElementById('thread-introduction').classList.add('hide');
      }

      // we will need the user details whether the user is posting a question
      // or just viewing a question so, load the user during init
      User.get_user().then(function(response) {
        var form = document.getElementById('question_form');
        form.addEventListener('submit', submit_comment);

        user = response;
        question_thread = document.getElementById('question-thread');

        // handle dialog close events
        dialog_handler();

        if (question_view) {
          var params = Utils.get_url_parameters(location);
          if (params.id) {

            Utils.toggle_spinner();

            question_id = params.id;
            show_question();
          }
        }
      });
    }
  };
  exports.QuestionController = QuestionController;
  QuestionController.init();

})(window);
<commit_msg>Fixes first question dialog for promise async storage<commit_after>'use strict';

/* global asyncStorage, SumoDB, Utils, User, nunjucks */

(function(exports) {
  var question_thread;
  var question_id;
  var dialog;
  var user;

  /**
   * Handles close button events from a fialog modal.
   */
  function dialog_handler() {
    dialog = document.querySelector('#first_question_help');
    var closeButton = dialog.querySelector('#confirm');

    closeButton.addEventListener('click', function(evt) {
      evt.preventDefault();
      dialog.classList.add('hide');
    });
  }

  /**
   * Adds the thread header that displays, the relative time the question was
   * posted, the device (if already categorised) and the display name of the
   * user who posted the question.
   * @param {object} question - The question JSON object
   */
  function add_thread_header(question) {
    var handset_type;
    var date_posted = Utils.time_since(new Date(question.updated));
    var author = question.creator.display_name || question.creator.username;

    for (var i = 0, l = question.metadata.length; i < l; i++) {
      var current_item = question.metadata[i];
      if (current_item.name === 'device_type') {
        handset_type = current_item.value;
        break;
      }
    }
    var html = nunjucks.render('thread_header.html', {
      date_posted: date_posted,
      handset_type: handset_type,
      author: author
    });
    question_thread.insertAdjacentHTML('afterbegin', html);
  }

  /**
   * Adds the footer section to the question thread. Currently this consist
   * of the receive new notifications settings chckbox.
   */
  function add_thread_footer() {
    var do_notify = user.settings[0].value === 'True' ? true : false;
    var html = nunjucks.render('thread_footer.html', {
      new_comment_notify: do_notify
    });
    question_thread.insertAdjacentHTML('beforeend', html);
  }

  function submit_comment(evt) {
    evt.preventDefault();
    var comment = document.getElementById('question_field').value;

    Utils.toggle_spinner();

    document.getElementById('thread-introduction').classList.add('hide');
    document.getElementById('question-thread').classList.remove('hide');

    var fake_comment = nunjucks.render('comment.html',
      {comment: {content: comment}});
    var list = document.getElementById('comment-list');
    var list_item = document.createElement('li');

    list.appendChild(list_item).innerHTML += fake_comment;
    list_item.scrollIntoView();

    var submit_promise;
    if (question_id) {
      submit_promise = submit_answer(question_id, comment);
    } else {
      submit_promise = submit_question(comment).then(function(comment) {

        add_thread_header(comment);
        add_thread_footer();

        comment.content = comment.title;
        return comment;
      });
    }

    submit_promise.then(function(comment) {

      Utils.toggle_spinner();

      // Only handle first time help message scenario for questions
      if (comment.answers) {
        asyncStorage.getItem('seen_first_question_help').then(function(response) {
          // See if the flag exist
          if (!response) {
            // flag does not exist, show the dialog and set the flag
            dialog.classList.remove('hide');
            asyncStorage.setItem('seen_first_question_help', true);
          }
        });
      }

      comment.created = Utils.time_since(new Date(comment.created));
      list_item.innerHTML = nunjucks.render('comment.html',
        {comment: comment});
      window.top.postMessage({question_id: question_id, comment: comment}, '*');
    });
  }

  function submit_question(comment) {
    return SumoDB.post_question(comment).then(function(response) {
      question_id = response.id;
      return response;
    });
  }

  function submit_answer(question_id, comment) {
    return SumoDB.post_answer(question_id, comment).then(function(response) {
      return response;
    });
  }

  /**
   * Adds an event listener to the new comment notify checkbox and updates
   * the current user's preference on change events.
   */
  function pref_change_listener() {
    var new_comments_notification = document.querySelector('#new_comments_notification');

    // only add event listener if the element exists
    if (new_comments_notification) {
      new_comments_notification.addEventListener('change', function() {
        User.set_preference('new_comment_notify', this.checked);
      });
    }
  }

  function show_question() {
    if (!question_id) {
      Utils.toggle_spinner();
      return;
    }

    document.getElementById('question-thread').classList.remove('hide');

    var question_content = [];
    question_content.push(SumoDB.get_question(question_id));

    question_content.push(SumoDB.get_answers_for_question(question_id));

    Promise.all(question_content).then(function([question, answers]) {

      Utils.toggle_spinner();

      question.content = question.title;
      answers.push(question);
      answers.reverse();

      for (var i = 0, l = answers.length; i < l; i++) {
        var created = answers[i].created;
        answers[i].created = Utils.time_since(new Date(created));
      }

      add_thread_header(question);
      var html = nunjucks.render('thread.html', {
        user: user,
        results: answers
      });
      var list = document.getElementById('comment-list');
      list.insertAdjacentHTML('beforeend', html);
      add_thread_footer();

      // listen for clicks on new comment notify checkbox
      pref_change_listener();
    });
  }

  var QuestionController = {
    init: function() {
      var question_view = location.search ? true : false;

      // if this is a question view, not arrived at from clicking/tapping
      // ask a question, hide the introduction section.
      if (question_view) {
        document.getElementById('thread-introduction').classList.add('hide');
      }

      // we will need the user details whether the user is posting a question
      // or just viewing a question so, load the user during init
      User.get_user().then(function(response) {
        var form = document.getElementById('question_form');
        form.addEventListener('submit', submit_comment);

        user = response;
        question_thread = document.getElementById('question-thread');

        // handle dialog close events
        dialog_handler();

        if (question_view) {
          var params = Utils.get_url_parameters(location);
          if (params.id) {

            Utils.toggle_spinner();

            question_id = params.id;
            show_question();
          }
        }
      });
    }
  };
  exports.QuestionController = QuestionController;
  QuestionController.init();

})(window);
<|endoftext|>


<commit_before>var util = require('../../utils/MyUtil');
var modStep = require('./modStep.js');
FLOW_ENGINE_CANCELED = false;
var FlowEngine = function(flow) {
	FLOW_ENGINE_CANCELED = false;
	var vars = {};
	var wv = null;
	this.setWv = function(v) {
		wv = v;
		return this;
	}
	var item = null;
	this.setItem = function(v) {
		item = v;
		return this;
	}
	var ctx = {};
	this.setContext = function(v) {
		ctx = v;
		wv = ctx.wv;
		item = ctx.item;
		return this;
	}
	this.setInputVars = function(v) {
		for(var i in v) {
			ctx.vars[i] = v[i];
		}
		return this;
	}
	this.flow = util.clone(flow);
	this.canceled = false;

	this.execute = function(done) {
		var steps = this.flow.steps;
		if(steps && steps.length) {
			var curStep = -1;
			var checkNext = function() {
				curStep++;
				if(curStep < steps.length) {
					processStep(steps[curStep], checkNext);
				}
				else {
					if(done.length == 1) {
						setTimeout(function() {
							var outputVars = {};
							for(var i in ctx.vars) {
								outputVars[i] = ctx.vars[i];
							}
							done(outputVars);
						}, 1);
					}
					else {
						setTimeout(done, 1);
					}
				}
			}
			setTimeout(checkNext, 10);
		}
	}
	this.cancel = function() {
		this.canceled = true;
	}
	var replaceVars = function(c) {
		for(var k in vars) {
			c = util.replaceAll(c, '##' + k + '##', vars[k]);
		}
		for(var k in ctx.vars) {
			c = util.replaceAll(c, '##' + k + '##', ctx.vars[k]);
		}
		return c;
	}
	var replaceVarsStep = function(step) {
		for(var i in step) {
			if(typeof step[i] != 'string') {
				step[i] = replaceVarsStep(step[i]);
			}
			else {
				step[i] = replaceVars(step[i]);
			}
		}
		return step;
	}
	var processStep = function(step, next) {
		//console.log('processStep ' + FLOW_ENGINE_CANCELED);
		//console.log(JSON.stringify(step));
		//console.log(JSON.stringify(ctx._vars));
		if(FLOW_ENGINE_CANCELED) {
			return;
		}
		step = replaceVarsStep(step);
		//console.log(JSON.stringify(step));
		// search ctx.flows if any
		if(typeof ctx.flows != 'undefined') {
			var flow = ctx.flows[step.type];
			//console.log('search flow ' + step.type + " = " + (typeof flow));
			if(typeof flow != 'undefined') {
				var v = util.clone(step);
				delete v.type;
				new FlowEngine(flow).setContext(ctx).setInputVars(v).execute(function() {
					setTimeout(next, 1);
				});
				return;
			}
		}
		// search step modules if any
		ctx._vars = vars; // get local vars
		modStep.processStep(ctx, step, next);
		vars = ctx._vars; // set local vars
	}
}

module.exports.FlowEngine = FlowEngine;
module.exports.FLOW_ENGINE_CANCELED = FLOW_ENGINE_CANCELED;<commit_msg>move util.clone delete type inside setInputVars<commit_after>var util = require('../../utils/MyUtil');
var modStep = require('./modStep.js');
FLOW_ENGINE_CANCELED = false;
var FlowEngine = function(flow) {
	FLOW_ENGINE_CANCELED = false;
	var vars = {};
	var wv = null;
	this.setWv = function(v) {
		wv = v;
		return this;
	}
	var item = null;
	this.setItem = function(v) {
		item = v;
		return this;
	}
	var ctx = {};
	this.setContext = function(v) {
		ctx = v;
		wv = ctx.wv;
		item = ctx.item;
		return this;
	}
	this.setInputVars = function(vars) {
		var v = util.clone(vars);
		delete v.type;
		for(var i in v) {
			ctx.vars[i] = v[i];
		}
		return this;
	}
	this.flow = util.clone(flow);
	this.canceled = false;

	this.execute = function(done) {
		var steps = this.flow.steps;
		if(steps && steps.length) {
			var curStep = -1;
			var checkNext = function() {
				curStep++;
				if(curStep < steps.length) {
					processStep(steps[curStep], checkNext);
				}
				else {
					if(done.length == 1) {
						setTimeout(function() {
							var outputVars = {};
							for(var i in ctx.vars) {
								outputVars[i] = ctx.vars[i];
							}
							done(outputVars);
						}, 1);
					}
					else {
						setTimeout(done, 1);
					}
				}
			}
			setTimeout(checkNext, 10);
		}
	}
	this.cancel = function() {
		this.canceled = true;
	}
	var replaceVars = function(c) {
		for(var k in vars) {
			c = util.replaceAll(c, '##' + k + '##', vars[k]);
		}
		for(var k in ctx.vars) {
			c = util.replaceAll(c, '##' + k + '##', ctx.vars[k]);
		}
		return c;
	}
	var replaceVarsStep = function(step) {
		for(var i in step) {
			if(typeof step[i] != 'string') {
				step[i] = replaceVarsStep(step[i]);
			}
			else {
				step[i] = replaceVars(step[i]);
			}
		}
		return step;
	}
	var processStep = function(step, next) {
		//console.log('processStep ' + FLOW_ENGINE_CANCELED);
		//console.log(JSON.stringify(step));
		//console.log(JSON.stringify(ctx._vars));
		if(FLOW_ENGINE_CANCELED) {
			return;
		}
		step = replaceVarsStep(step);
		//console.log(JSON.stringify(step));
		// search ctx.flows if any
		if(typeof ctx.flows != 'undefined') {
			var flow = ctx.flows[step.type];
			//console.log('search flow ' + step.type + " = " + (typeof flow));
			if(typeof flow != 'undefined') {
				new FlowEngine(flow).setContext(ctx).setInputVars(step).execute(function() {
					setTimeout(next, 1);
				});
				return;
			}
		}
		// search step modules if any
		ctx._vars = vars; // get local vars
		modStep.processStep(ctx, step, next);
		vars = ctx._vars; // set local vars
	}
}

module.exports.FlowEngine = FlowEngine;
module.exports.FLOW_ENGINE_CANCELED = FLOW_ENGINE_CANCELED;<|endoftext|>


<commit_before>/**
 * rudiment - A simple RESTful resource manager
 * https://github.com/gavinhungry/rudiment
 */

(function() {
  'use strict';

  var Rudiment = function(opts) {

  };

  module.exports = Rudiment;

})();
<commit_msg>Fill out methods<commit_after>/**
 * rudiment - A simple RESTful resource manager
 * https://github.com/gavinhungry/rudiment
 */

(function() {
  'use strict';

  /**
   * @constructor for Rudiments
   *
   * Rudiment objects have prototype methods that can be overridden as needed.
   * By default, the database should support a subset of MongoDB API methods
   * (for example, NeDB works well here).
   *
   * The schema is a predicate function that determines the validity of a
   * candidate document. The props array is filled automatically if you pass
   * an object from js-schema here.
   *
   * @param {Object} opts
   * @param {Object} opts.db - database
   * @param {Function} [opts.schema] - predicate function for schema validation
   * @param {Array} [opts.props] - document properties to filter
   * @param {String} [opts.key] - document key in database (defaults to first prop or '_id')
   * @param {Array} [opts.uniq] - document properties to consider unique
   */
  var Rudiment = function(opts) {
    var that = this;
    opts = opts || {};

    this._db = opts.db;
    if (!this._db) {
      throw new Error('No database provided');
    }

    this._schema = opts.schema;

    // attempt to get props from the passed schema
    var schemaProps;
    if (this._schema && this._schema.toJSON) {
      var obj = this._schema.toJSON();
      if (obj && obj.properties) {
        schemaProps = Object.keys(obj.properties);
      }
    }

    this._key = opts.key || (schemaProps ? schemaProps[0] : null) || '_id';
    this._props = opts.props || schemaProps;
    this._uniq = opts.uniq || [this._key];

    // allow the constructor to override any of the prototype methods
    Object.keys(Rudiment.prototype).forEach(function(method) {
      if (opts[method]) {
        that[method] = opts[method];
      }
    });
  };

  Rudiment.prototype = {
    /**
     * Get a copy of a document with extraneous properties removed
     *
     * @param {Object} doc - a document to clean
     * @return {Object} a copy of the cleaned document
     */
    clean: function(doc) {
      if (!this._props) {
        return doc;
      }

      return this._props.reduce(function(obj, prop) {
        obj[prop] = doc[prop];
        return obj;
      }, {});
    },

    /**
     * Check if a document is valid
     *
     * A document is valid if it passes the schema. If no schema is defined,
     * always returns true.
     *
     * @param {Object} doc - a document to test
     * @return {Boolean}
     */
    valid: function(doc) {
      return this._schema ? this._schema(doc) : true;
    },

    /**
     * Check if a document is admissible
     *
     * A document is admissible if it passes the valid predicate, and no other
     * document in the database is already using its unique keys.
     *
     * @param {Object} doc - a document to test
     * @param {Function} callback(err, {Boolean})
     */
    admissible: function(doc, callback) {
      if (!this.valid(doc)) {
        callback(null, false);
        return;
      }

      this._db.find({
        $or: this._uniq.map(function(uniq) {
          return o(uniq, doc[uniq]);
        })
      }, function(err, docs) {
        if (err) {
          return callback(err, false);
        }

        callback(null, !docs.length);
      });
    },

    /**
     * Insert a new document into the database
     *
     * @param {Object} doc - a document to insert
     * @param {Function} callback(err, {Object|null})
     */
    create: function(doc, callback) {
      var that = this;

      this.admissible(doc, function(err, ok) {
        if (!ok) {
          return callback(err, null);
        }

        doc = that.clean(doc);
        that._db.insert(doc, callback);
      });
    },

    /**
     * Get a document from the database by key
     *
     * @param {Mixed} id - key for document to get
     * @param {Function} callback(err, {Object|null})
     */
    read: function(id, callback) {
      this._db.findOne(o(this._key, id), callback);
    },

    /**
     * Get all documents from the database
     *
     * @param {Function} callback(err, {Array})
     */
    readAll: function(callback) {
      this._db.find({}, callback);
    },

    /**
     * Update a document in the database by key
     *
     * The key and unique values of a document cannot be updated. Remove the
     * document and create a new document instead.
     *
     * @param {Mixed} id - key for document to update
     * @param {Object} updates - updates to apply to document
     * @param {Function} callback(err, {Number})
     */
    update: function(id, updates, callback) {
      var that = this;
      updates = updates || {};

      this.read(id, function(err, doc) {
        if (err) {
          return callback(err);
        }

        if (!doc) {
          return callback(null, 0);
        }

        Object.keys(doc).forEach(function(prop) {
          if (updates.hasOwnProperty(prop)) {
            doc[prop] = updates[prop];
          }
        });

        if (that.valid(doc)) {
          // remove key and unique properties
          delete doc[that._key];
          that._uniq.forEach(function(uniq) {
            delete doc[uniq];
          });

          return that._db.update(o(that._key, id), {
            $set: doc
          }, callback);
        }

        callback(null, 0);
      });
    },

    /**
     * Delete a document from the database
     *
     * @param {Mixed} id - key for document to delete
     * @param {Function} callback(err, {Number})
     */
    delete: function(id, callback) {
      this._db.remove(o(this._key, id), callback);
    }
  };

  /**
   * Create an object with a variable key
   *
   * @param {Mixed} key
   * @param {Mixed} value
   * @return {Object}
   */
  var o = function(key, value) {
    var obj = {};
    obj[key] = value;

    return obj;
  };

  module.exports = Rudiment;

})();
<|endoftext|>


<commit_before>/**
 * rudiment - A simple CRUD resource manager
 * https://github.com/gavinhungry/rudiment
 */

(function() {
  'use strict';

  /**
   * @constructor for Rudiment objects
   *
   * Rudiment objects have prototype methods that can be overridden as needed.
   * By default, the database should support a subset of MongoDB API methods.
   *
   * The schema is a predicate function that determines the validity of a
   * candidate document. The props array is filled automatically if possible.
   *
   * @param {Object} opts
   * @param {Object} opts.db - database
   * @param {Function} [opts.schema] - predicate function for schema validation
   * @param {Array} [opts.props] - document properties to filter
   * @param {String} [opts.key] - document key in database (defaults to first prop or '_id')
   * @param {Array} [opts.uniq] - document properties to consider unique
   */
  var Rudiment = function(opts) {
    var that = this;
    opts = opts || {};

    this._db = opts.db;
    if (!this._db) {
      throw new Error('No database provided');
    }

    this._schema = opts.schema;

    // attempt to get props from the passed schema
    var schemaProps;
    if (this._schema && this._schema.toJSON) {
      var obj = this._schema.toJSON();
      if (obj && obj.properties) {
        schemaProps = Object.keys(obj.properties);
      }
    }

    this._path = opts.path;
    this._key = opts.key || (schemaProps ? schemaProps[0] : null) || '_id';
    this._props = opts.props || schemaProps;
    this._uniq = opts.uniq || [this._key];

    // allow the constructor to override any of the prototype methods
    Object.keys(Rudiment.prototype).forEach(function(method) {
      if (opts[method]) {
        that[method] = opts[method];
      }
    });
  };

  Rudiment.prototype = {
    /**
     * Remove extraneous properties from a document
     *
     * @param {Object} doc - a document to clean
     * @return {Object} a copy of the cleaned document
     */
    clean: function(doc) {
      if (!this._props) {
        return doc;
      }

      return this._props.reduce(function(obj, prop) {
        obj[prop] = doc[prop];
        return obj;
      }, {});
    },

    /**
     * Check if a document is valid
     *
     * A document is valid if it passes the schema. If no schema is defined,
     * always returns true.
     *
     * @param {Object} doc - a document to test
     * @return {Boolean}
     */
    valid: function(doc) {
      return this._schema ? this._schema(doc) : true;
    },

    /**
     * Check if a document is admissible
     *
     * A document is admissible if it passes the valid predicate, and no other
     * document in the database is already using its unique keys.
     *
     * @param {Object} doc - a document to test
     * @param {Function} callback(err, {Boolean})
     */
    admissible: function(doc, callback) {
      if (!this.valid(doc)) {
        callback(null, false);
        return;
      }

      this._db.find({
        $or: this._uniq.map(function(uniq) {
          return o(uniq, doc[uniq]);
        })
      }, function(err, docs) {
        if (err) {
          return callback(err, false);
        }

        callback(null, !docs.length);
      });
    },

    /**
     * REST handler
     *
     * @param {ServerResponse} res
     * @param {Function} [map] - map data before responding
     */
    rest: function(res, map) {
      var that = this;

      return function(err, data, method) {
        if (err) {
          return res.status(500).end();
        }

        if (typeof map === 'function') {
          data = map(data);
        }

        if (method === 'create') {
          return data ?
            res.status(201)
              .header('Location', '/' + that._path + '/' + data[that._key])
              .json(data) :
            res.status(409).end();
        }

        return data ?
          res.status(200).json(data) :
          res.status(404).end();
      };
    },

    /**
     * Insert a new document into the database
     *
     * @param {Object} doc - a document to insert
     * @param {Function} callback(err, {Object|null})
     */
    create: function(doc, callback) {
      var that = this;
      var method = 'create';

      this.admissible(doc, function(err, ok) {
        if (!ok) {
          return callback(err, null, method);
        }

        doc = that.clean(doc);
        that._db.insert(doc, function(err, doc) {
          callback(err, doc, method);
        });
      });
    },

    /**
     * Get a document from the database by key
     *
     * @param {Mixed} id - key for document to get
     * @param {Function} callback(err, {Object|null})
     */
    read: function(id, callback) {
      var method = 'read';

      this._db.findOne(o(this._key, id), function(err, doc) {
        callback(err, doc, method);
      });
    },

    /**
     * Get all documents from the database
     *
     * @param {Function} callback(err, {Array})
     */
    readAll: function(callback) {
      var method = 'readAll';

      this._db.find({}, function(err, docs) {
        callback(err, docs, method);
      });
    },

    /**
     * Update a document in the database by key
     *
     * The key and unique values of a document cannot be updated. Remove the
     * document and create a new document instead.
     *
     * @param {Mixed} id - key for document to update
     * @param {Object} updates - updates to apply to document
     * @param {Function} callback(err, {Boolean})
     */
    update: function(id, updates, callback) {
      var that = this;
      updates = updates || {};
      var method = 'update';

      this.read(id, function(err, doc) {
        if (err) {
          return callback(err, null, method);
        }

        if (!doc) {
          return callback(null, false, method);
        }

        Object.keys(doc).forEach(function(prop) {
          if (updates.hasOwnProperty(prop)) {
            doc[prop] = updates[prop];
          }
        });

        if (that.valid(doc)) {
          // remove key and unique properties
          delete doc[that._key];
          that._uniq.forEach(function(uniq) {
            delete doc[uniq];
          });

          return that._db.update(o(that._key, id), {
            $set: doc
          }, function(err, num) {
            if (err) {
              return callback(err, null, method);
            }

            callback(null, num > 0, method);
          });
        }

        callback(null, false, method);
      });
    },

    /**
     * Delete a document from the database
     *
     * @param {Mixed} id - key for document to delete
     * @param {Function} callback(err, {Boolean})
     */
    delete: function(id, callback) {
      var method = 'delete';

      this._db.remove(o(this._key, id), function(err, num) {
        if (err) {
          return callback(err, null, method);
        }

        callback(null, num > 0, method);
      });
    }
  };

  /**
   * Create an object with a variable key
   *
   * @param {Mixed} key
   * @param {Mixed} value
   * @return {Object}
   */
  var o = function(key, value) {
    var obj = {};
    obj[key] = value;

    return obj;
  };

  module.exports = Rudiment;

})();
<commit_msg>Simplify REST handler<commit_after>/**
 * rudiment - A simple CRUD resource manager
 * https://github.com/gavinhungry/rudiment
 */

(function() {
  'use strict';

  /**
   * @constructor for Rudiment objects
   *
   * Rudiment objects have prototype methods that can be overridden as needed.
   * By default, the database should support a subset of MongoDB API methods.
   *
   * The schema is a predicate function that determines the validity of a
   * candidate document. The props array is filled automatically if possible.
   *
   * @param {Object} opts
   * @param {Object} opts.db - database
   * @param {Function} [opts.schema] - predicate function for schema validation
   * @param {Array} [opts.props] - document properties to filter
   * @param {String} [opts.key] - document key in database (defaults to first prop or '_id')
   * @param {Array} [opts.uniq] - document properties to consider unique
   */
  var Rudiment = function(opts) {
    var that = this;
    opts = opts || {};

    this._db = opts.db;
    if (!this._db) {
      throw new Error('No database provided');
    }

    this._schema = opts.schema;

    // attempt to get props from the passed schema
    var schemaProps;
    if (this._schema && this._schema.toJSON) {
      var obj = this._schema.toJSON();
      if (obj && obj.properties) {
        schemaProps = Object.keys(obj.properties);
      }
    }

    this._path = opts.path;
    this._key = opts.key || (schemaProps ? schemaProps[0] : null) || '_id';
    this._props = opts.props || schemaProps;
    this._uniq = opts.uniq || [this._key];

    // allow the constructor to override any of the prototype methods
    Object.keys(Rudiment.prototype).forEach(function(method) {
      if (opts[method]) {
        that[method] = opts[method];
      }
    });
  };

  Rudiment.prototype = {
    /**
     * Remove extraneous properties from a document
     *
     * @param {Object} doc - a document to clean
     * @return {Object} a copy of the cleaned document
     */
    clean: function(doc) {
      if (!this._props) {
        return doc;
      }

      return this._props.reduce(function(obj, prop) {
        obj[prop] = doc[prop];
        return obj;
      }, {});
    },

    /**
     * Check if a document is valid
     *
     * A document is valid if it passes the schema. If no schema is defined,
     * always returns true.
     *
     * @param {Object} doc - a document to test
     * @return {Boolean}
     */
    valid: function(doc) {
      return this._schema ? this._schema(doc) : true;
    },

    /**
     * Check if a document is admissible
     *
     * A document is admissible if it passes the valid predicate, and no other
     * document in the database is already using its unique keys.
     *
     * @param {Object} doc - a document to test
     * @param {Function} callback(err, {Boolean})
     */
    admissible: function(doc, callback) {
      if (!this.valid(doc)) {
        callback(null, false);
        return;
      }

      this._db.find({
        $or: this._uniq.map(function(uniq) {
          return o(uniq, doc[uniq]);
        })
      }, function(err, docs) {
        if (err) {
          return callback(err, false);
        }

        callback(null, !docs.length);
      });
    },

    /**
     * REST handler
     *
     * @param {ServerResponse} res
     * @param {Function} [map] - map data before responding
     */
    rest: function(res, map) {
      var that = this;

      return function(err, data, create) {
        if (err) {
          return res.status(500).end();
        }

        if (typeof map === 'function') {
          data = map(data);
        }

        if (create) {
          return data ?
            res.status(201)
              .header('Location', '/' + that._path + '/' + data[that._key])
              .json(data) :
            res.status(409).end();
        }

        return data ?
          res.status(200).json(data) :
          res.status(404).end();
      };
    },

    /**
     * Insert a new document into the database
     *
     * @param {Object} doc - a document to insert
     * @param {Function} callback(err, {Object|null})
     */
    create: function(doc, callback) {
      var that = this;

      this.admissible(doc, function(err, ok) {
        if (!ok) {
          return callback(err, null, true);
        }

        doc = that.clean(doc);
        that._db.insert(doc, function(err, doc) {
          callback(err, doc, true);
        });
      });
    },

    /**
     * Get a document from the database by key
     *
     * @param {Mixed} id - key for document to get
     * @param {Function} callback(err, {Object|null})
     */
    read: function(id, callback) {
      this._db.findOne(o(this._key, id), callback);
    },

    /**
     * Get all documents from the database
     *
     * @param {Function} callback(err, {Array})
     */
    readAll: function(callback) {
      this._db.find({}, callback);
    },

    /**
     * Update a document in the database by key
     *
     * The key and unique values of a document cannot be updated. Remove the
     * document and create a new document instead.
     *
     * @param {Mixed} id - key for document to update
     * @param {Object} updates - updates to apply to document
     * @param {Function} callback(err, {Boolean})
     */
    update: function(id, updates, callback) {
      var that = this;
      updates = updates || {};

      this.read(id, function(err, doc) {
        if (err) {
          return callback(err, null);
        }

        if (!doc) {
          return callback(null, false);
        }

        Object.keys(doc).forEach(function(prop) {
          if (updates.hasOwnProperty(prop)) {
            doc[prop] = updates[prop];
          }
        });

        if (that.valid(doc)) {
          // remove key and unique properties
          delete doc[that._key];
          that._uniq.forEach(function(uniq) {
            delete doc[uniq];
          });

          return that._db.update(o(that._key, id), {
            $set: doc
          }, function(err, num) {
            if (err) {
              return callback(err, null);
            }

            callback(null, num > 0);
          });
        }

        callback(null, false);
      });
    },

    /**
     * Delete a document from the database
     *
     * @param {Mixed} id - key for document to delete
     * @param {Function} callback(err, {Boolean})
     */
    delete: function(id, callback) {
      this._db.remove(o(this._key, id), function(err, num) {
        if (err) {
          return callback(err, null);
        }

        callback(null, num > 0);
      });
    }
  };

  /**
   * Create an object with a variable key
   *
   * @param {Mixed} key
   * @param {Mixed} value
   * @return {Object}
   */
  var o = function(key, value) {
    var obj = {};
    obj[key] = value;

    return obj;
  };

  module.exports = Rudiment;

})();
<|endoftext|>


<commit_before>angular.module( 'App.Views' ).controller( 'Dashboard.AnalyticsCtrl', function(
<<<<<<< HEAD
	$scope, $state, App, Api, Payload, Game, Game_Package, Game_Release, Graph, SiteAnalytics, Geo, gettextCatalog, payload )
=======
	$scope, $state, App, Api, Payload, Game, Game_Package, Game_Release, Graph, SiteAnalytics, gettextCatalog, payload )
>>>>>>> master
{
	var _this = this;

	/**
	 * When the page first loads we check the state params to make sure they are all filled in.
	 * If they weren't, we change the URL. When this happens it'll replace the previous URL
	 * unless we skip the replacement. After our initial params are bootstrapped it's fine to
	 * replace the URL from there on out.
	 */
	var paramsBootstrapped = false;

	App.title = gettextCatalog.getString( 'Analytics' );

	this.games = Game.populate( payload.games );
	this.breakdownReports = [];

	this.stats = {
		view: {
			eventType: 'view',
			label: gettextCatalog.getString( 'Views' ),
			type: 'number',
		},
		download: {
			eventType: 'download',
			label: gettextCatalog.getString( 'Downloads' ),
			type: 'number',
		},
		// install: {
		// 	eventType: 'install',
		// 	label: gettextCatalog.getString( 'Installs' ),
		// 	type: 'number',
		// },
		comment: {
			eventType: 'comment',
			label: gettextCatalog.getString( 'Comments' ),
			type: 'number',
		},
		rating: {
			eventType: 'rating',
			label: gettextCatalog.getString( 'Ratings' ),
			type: 'number',
		},
		follow: {
			eventType: 'follow',
			label: gettextCatalog.getString( 'Follows' ),
			type: 'number',
		},
		// sale: {
		// 	eventType: 'sale',
		// 	label: gettextCatalog.getString( 'Sales' ),
		// 	type: 'number',
		// },
		// revenue: {
		// 	eventType: 'revenue',
		// 	label: gettextCatalog.getString( 'Revenue' ),
		// 	type: 'currency',
		// },
	};

	var _gameEventTypes = Object.keys( this.stats );
	var _packageEventTypes = [ 'download' ];
	var _releaseEventTypes = [ 'download' ];

	this.stateChanged = function( $stateParams )
	{
		this.period = $stateParams.period || 'monthly';
		this.resource = $stateParams.resource || 'Game';
		this.resourceId = parseInt( $stateParams.resourceId, 10 ) || this.games[0].id;

		if ( this.resource == 'Game' ) {
			this.eventTypes = _gameEventTypes;
			this.eventType = $stateParams.eventType || 'view';
		}
		else if ( this.resource == 'Game_Package' ) {
			this.eventTypes = _packageEventTypes;
			this.eventType = $stateParams.eventType || 'download';
		}
		else if ( this.resource == 'Game_Release' ) {
			this.eventTypes = _releaseEventTypes;
			this.eventType = $stateParams.eventType || 'download';
		}
		else {
			throw new Error( 'Invalid resource.' );
		}

		this.stat = this.stats[ this.eventType ];

		// If any of the parameters changed, refresh the state.
		if (
			this.period != $stateParams.period
			|| this.eventType != $stateParams.eventType
			|| this.resource != $stateParams.resource
			|| this.resourceId != $stateParams.resourceId
		) {
			var options = {};
			if ( paramsBootstrapped ) {
				options = { location: 'replace' };
			}

			$state.go(
				'dashboard.analytics.view',
				_.pick( this, [ 'period', 'eventType', 'resource', 'resourceId' ] ),
				options
			);

			paramsBootstrapped = true;
			return;
		}

		paramsBootstrapped = true;

		this.now = Date.now();
		this.startTime = null;
		this.endTime = null;

		this.prevMonth = null;
		this.prevYear = null;

		this.nextMonth = null;
		this.nextYear = null;
		if ( this.period == 'monthly' ) {

			var date = new Date();
			var year, month;
			if ( !$stateParams.year || !$stateParams.month ) {
				year = date.getFullYear();
				month = date.getMonth();
			}
			else {
				year = parseInt( $stateParams.year, 10 );
				month = parseInt( $stateParams.month, 10 );
			}

			this.startTime = (new Date( year, month, 1 )).getTime();
<<<<<<< HEAD
			this.endTime = (new Date( year, month + 1, 0 )).getTime();
=======
			this.endTime = (new Date( year, month + 1, 1 )).getTime() - 1;
>>>>>>> master

			this.prevMonth = (new Date( year, month - 1, 1 )).getMonth();
			this.prevYear = (new Date( year, month - 1, 1 )).getFullYear();

			this.nextMonth = (new Date( year, month + 1, 1 )).getMonth();
			this.nextYear = (new Date( year, month + 1, 1 )).getFullYear();
		}
	};

	var globalWatch = [
		'analyticsCtrl.period',
		'analyticsCtrl.resource',
		'analyticsCtrl.resourceId',
		'analyticsCtrl.startTime',
		'analyticsCtrl.endTime',
	];

	$scope.$watchGroup( [ 'analyticsCtrl.resource', 'analyticsCtrl.resourceId' ], function()
	{
		Api.sendRequest( '/web/dash/analytics/get-resource/' + _this.resource + '/' + _this.resourceId )
			.then( function( _payload )
			{
				_this.game = _payload.game ? new Game( _payload.game ) : null;
				_this.package = _payload.package ? new Game_Package( _payload.package ) : null;
				_this.release = _payload.release ? new Game_Release( _payload.release ) : null;
			} );
	} );

	// Only the fields that can affect histograms.
	$scope.$watchGroup( globalWatch, function()
	{
		if ( _this.resource && _this.resourceId ) {
			if ( _this.period == 'all' ) {
				_this.counts();
			}
			else if ( _this.period == 'monthly' ) {
				_this.histograms();
			}
		}
	} );

	// Only the fields that can affect report breakdowns.
	$scope.$watchGroup( globalWatch.concat( [
		'analyticsCtrl.eventType',
	] ),
	function()
	{
		if ( _this.period && _this.resource && _this.resourceId && _this.eventType ) {
			_this.changeReport( _this.stat );
		}
	} );

	this.histograms = function()
	{
		return SiteAnalytics.getHistogram( _this.resource, _this.resourceId, this.eventTypes, [ this.startTime, this.endTime ] )
			.then( function( data )
			{
				angular.extend( _this, data );
			} );
	};

	this.counts = function()
	{
		return SiteAnalytics.getCount( _this.resource, _this.resourceId, this.eventTypes )
			.then( function( data )
			{
				angular.extend( _this, data );
			} );
	};

	this.getReport = function( field, title, fieldLabel, extraRequestData )
	{
<<<<<<< HEAD
		var type = 'top';
		if ( field == 'rating' ) {
			type = 'rating-breakdown';
		}

		var report = {
			type: type,
			title: title,
			field: field,
			fieldLabel: fieldLabel,
			isLoaded: false,
		};

		this.breakdownReports.push( report );

		var date = new Date();
		var request = {
			top: {
				target: _this.resource,
				target_id: this.resourceId,
				collection: this.eventType + 's',
				analyzer: 'top-composition',
				field: field,
				from_date: this.startTime / 1000,
				to_date: this.endTime / 1000,
				timezone: date.getTimezoneOffset(),
			},
		};

		if ( extraRequestData ) {
			angular.extend( request.top, extraRequestData );
		}

		Api.sendRequest( '/web/dash/analytics/display', request, { sanitizeComplexData: false } )
			.then( function( response )
			{
				var graph = null;

				// country code => country name
				if ( field == 'country' ) {
					var data = {};
					angular.forEach( response.top.result, function( val, key )
					{
						if ( key == 'other' ) {
							data[ gettextCatalog.getString( 'Unknown' ) ] = val;
						}
						else {
							data[ Geo.getCountryName( key ) || key ] = val;
						}
					} );
					response.top.result = data;
				}

				if ( field != 'rating' && field != 'source_url' ) {

					graph = [];

					// Pull the top 3 results so we can push into a pie chart if desired.
					var i = 0, total = 0;
					angular.forEach( response.top.result, function( val, key )
					{
						if ( i < 3 ) {
							graph.push( {
								label: key,
								value: val,
							} );
							total += val;
						}
						++i;
					} );

				}

				if ( field == 'rating' ) {
					var data = {};
					[ 1, 2, 3, 4, 5 ].forEach( function( rating )
					{
						data[ rating ] = response.top.result[ rating ] || 0;
					} );
					response.top.result = data;
				}

				report.data = response.top.result;
				report.graph = graph;
				report.total = response.top.total;
				report.isLoaded = true;
				report.hasData = report.data && Object.keys( report.data ).length > 0;
			} );
=======
		var report = SiteAnalytics.getReport( field, title, fieldLabel, {
			resource: this.resource,
			resourceId: this.resourceId,
			eventType: this.eventType,
			startTime: this.startTime,
			endTime: this.endTime,
		} );

		this.breakdownReports.push( report );
>>>>>>> master
	};

	this.changeReport = function( stat )
	{
		this.eventType = stat.eventType;
		this.breakdownReports = [];

		if ( this.eventType == 'download' ) {
			this.getReport( 'os', 'Operating Systems', 'OS' );
		}

		if ( this.eventType == 'view' || this.eventType == 'download' ) {
<<<<<<< HEAD
			this.getReport( 'source', 'Top Sources', 'Domain', { conditions: [ 'source-gamejolt' ] } );
=======
			this.getReport( 'source', 'Top Sources', 'Domain' );
>>>>>>> master
			this.getReport( 'source_url', 'Referring Pages', 'Page' );
			this.getReport( 'country', 'Countries', 'Country' );
		}

		if ( this.eventType == 'install' ) {
			this.getReport( 'os', 'Operating Systems', 'OS' );
			this.getReport( 'country', 'Countries', 'Country' );
		}

		if ( this.eventType == 'comment' ) {
			this.getReport( 'comment_language', 'Languages', 'Language' );
		}

		if ( this.eventType == 'rating' ) {
			this.getReport( 'rating', 'Rating Breakdown', 'Rating' );
		}

		if ( this.eventType == 'follow' ) {
			this.getReport( 'country', 'Countries', 'Country' );
		}
	};
} );
<commit_msg>Fix bad merge.<commit_after>angular.module( 'App.Views' ).controller( 'Dashboard.AnalyticsCtrl', function(
	$scope, $state, App, Api, Payload, Game, Game_Package, Game_Release, Graph, SiteAnalytics, gettextCatalog, payload )
{
	var _this = this;

	/**
	 * When the page first loads we check the state params to make sure they are all filled in.
	 * If they weren't, we change the URL. When this happens it'll replace the previous URL
	 * unless we skip the replacement. After our initial params are bootstrapped it's fine to
	 * replace the URL from there on out.
	 */
	var paramsBootstrapped = false;

	App.title = gettextCatalog.getString( 'Analytics' );

	this.games = Game.populate( payload.games );
	this.breakdownReports = [];

	this.stats = {
		view: {
			eventType: 'view',
			label: gettextCatalog.getString( 'Views' ),
			type: 'number',
		},
		download: {
			eventType: 'download',
			label: gettextCatalog.getString( 'Downloads' ),
			type: 'number',
		},
		// install: {
		// 	eventType: 'install',
		// 	label: gettextCatalog.getString( 'Installs' ),
		// 	type: 'number',
		// },
		comment: {
			eventType: 'comment',
			label: gettextCatalog.getString( 'Comments' ),
			type: 'number',
		},
		rating: {
			eventType: 'rating',
			label: gettextCatalog.getString( 'Ratings' ),
			type: 'number',
		},
		follow: {
			eventType: 'follow',
			label: gettextCatalog.getString( 'Follows' ),
			type: 'number',
		},
		// sale: {
		// 	eventType: 'sale',
		// 	label: gettextCatalog.getString( 'Sales' ),
		// 	type: 'number',
		// },
		// revenue: {
		// 	eventType: 'revenue',
		// 	label: gettextCatalog.getString( 'Revenue' ),
		// 	type: 'currency',
		// },
	};

	var _gameEventTypes = Object.keys( this.stats );
	var _packageEventTypes = [ 'download' ];
	var _releaseEventTypes = [ 'download' ];

	this.stateChanged = function( $stateParams )
	{
		this.period = $stateParams.period || 'monthly';
		this.resource = $stateParams.resource || 'Game';
		this.resourceId = parseInt( $stateParams.resourceId, 10 ) || this.games[0].id;

		if ( this.resource == 'Game' ) {
			this.eventTypes = _gameEventTypes;
			this.eventType = $stateParams.eventType || 'view';
		}
		else if ( this.resource == 'Game_Package' ) {
			this.eventTypes = _packageEventTypes;
			this.eventType = $stateParams.eventType || 'download';
		}
		else if ( this.resource == 'Game_Release' ) {
			this.eventTypes = _releaseEventTypes;
			this.eventType = $stateParams.eventType || 'download';
		}
		else {
			throw new Error( 'Invalid resource.' );
		}

		this.stat = this.stats[ this.eventType ];

		// If any of the parameters changed, refresh the state.
		if (
			this.period != $stateParams.period
			|| this.eventType != $stateParams.eventType
			|| this.resource != $stateParams.resource
			|| this.resourceId != $stateParams.resourceId
		) {
			var options = {};
			if ( paramsBootstrapped ) {
				options = { location: 'replace' };
			}

			$state.go(
				'dashboard.analytics.view',
				_.pick( this, [ 'period', 'eventType', 'resource', 'resourceId' ] ),
				options
			);

			paramsBootstrapped = true;
			return;
		}

		paramsBootstrapped = true;

		this.now = Date.now();
		this.startTime = null;
		this.endTime = null;

		this.prevMonth = null;
		this.prevYear = null;

		this.nextMonth = null;
		this.nextYear = null;
		if ( this.period == 'monthly' ) {

			var date = new Date();
			var year, month;
			if ( !$stateParams.year || !$stateParams.month ) {
				year = date.getFullYear();
				month = date.getMonth();
			}
			else {
				year = parseInt( $stateParams.year, 10 );
				month = parseInt( $stateParams.month, 10 );
			}

			this.startTime = (new Date( year, month, 1 )).getTime();
			this.endTime = (new Date( year, month + 1, 1 )).getTime() - 1;

			this.prevMonth = (new Date( year, month - 1, 1 )).getMonth();
			this.prevYear = (new Date( year, month - 1, 1 )).getFullYear();

			this.nextMonth = (new Date( year, month + 1, 1 )).getMonth();
			this.nextYear = (new Date( year, month + 1, 1 )).getFullYear();
		}
	};

	var globalWatch = [
		'analyticsCtrl.period',
		'analyticsCtrl.resource',
		'analyticsCtrl.resourceId',
		'analyticsCtrl.startTime',
		'analyticsCtrl.endTime',
	];

	$scope.$watchGroup( [ 'analyticsCtrl.resource', 'analyticsCtrl.resourceId' ], function()
	{
		Api.sendRequest( '/web/dash/analytics/get-resource/' + _this.resource + '/' + _this.resourceId )
			.then( function( _payload )
			{
				_this.game = _payload.game ? new Game( _payload.game ) : null;
				_this.package = _payload.package ? new Game_Package( _payload.package ) : null;
				_this.release = _payload.release ? new Game_Release( _payload.release ) : null;
			} );
	} );

	// Only the fields that can affect histograms.
	$scope.$watchGroup( globalWatch, function()
	{
		if ( _this.resource && _this.resourceId ) {
			if ( _this.period == 'all' ) {
				_this.counts();
			}
			else if ( _this.period == 'monthly' ) {
				_this.histograms();
			}
		}
	} );

	// Only the fields that can affect report breakdowns.
	$scope.$watchGroup( globalWatch.concat( [
		'analyticsCtrl.eventType',
	] ),
	function()
	{
		if ( _this.period && _this.resource && _this.resourceId && _this.eventType ) {
			_this.changeReport( _this.stat );
		}
	} );

	this.histograms = function()
	{
		return SiteAnalytics.getHistogram( _this.resource, _this.resourceId, this.eventTypes, [ this.startTime, this.endTime ] )
			.then( function( data )
			{
				angular.extend( _this, data );
			} );
	};

	this.counts = function()
	{
		return SiteAnalytics.getCount( _this.resource, _this.resourceId, this.eventTypes )
			.then( function( data )
			{
				angular.extend( _this, data );
			} );
	};

	this.getReport = function( field, title, fieldLabel, extraRequestData )
	{
		var report = SiteAnalytics.getReport( field, title, fieldLabel, {
			resource: this.resource,
			resourceId: this.resourceId,
			eventType: this.eventType,
			startTime: this.startTime,
			endTime: this.endTime,
		} );

		this.breakdownReports.push( report );
	};

	this.changeReport = function( stat )
	{
		this.eventType = stat.eventType;
		this.breakdownReports = [];

		if ( this.eventType == 'download' ) {
			this.getReport( 'os', 'Operating Systems', 'OS' );
		}

		if ( this.eventType == 'view' || this.eventType == 'download' ) {
			this.getReport( 'source', 'Top Sources', 'Domain' );
			this.getReport( 'source_url', 'Referring Pages', 'Page' );
			this.getReport( 'country', 'Countries', 'Country' );
		}

		if ( this.eventType == 'install' ) {
			this.getReport( 'os', 'Operating Systems', 'OS' );
			this.getReport( 'country', 'Countries', 'Country' );
		}

		if ( this.eventType == 'comment' ) {
			this.getReport( 'comment_language', 'Languages', 'Language' );
		}

		if ( this.eventType == 'rating' ) {
			this.getReport( 'rating', 'Rating Breakdown', 'Rating' );
		}

		if ( this.eventType == 'follow' ) {
			this.getReport( 'country', 'Countries', 'Country' );
		}
	};
} );
<|endoftext|>


<commit_before>/*
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      This file is part of the Smart Developer Hub Project:
        http://www.smartdeveloperhub.org/
      Center for Open Middleware
            http://www.centeropenmiddleware.com/
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      Copyright (C) 2015 Center for Open Middleware.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      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
                http://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.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
*/

(function() {

    var defaultConfig = {
        height: {
            type: ['number'],
            default: 100
        },
        color: {
            type: ['function', 'object'],
            default: function() { return "#000000"; }
        }
    };

    /* TimeBar constructor
     *   element: the DOM element that will contain the widget
     *   resources: resources to observe
     *   contexts: list of contexts
     *   configuration: custom widget configuration.
     */
    var TimeBar = function TimeBar(element, resources, contexts, configuration) {

        this.element = $(element); //Store as jquery object

        // Extending widget
        framework.widgets.CommonWidget.call(this, false, element);

        // Configuration
        this.configuration = this.normalizeConfig(defaultConfig, configuration);

        // Use the callback offered by widget.common
        this.observeCallback = this.commonObserveCallback.bind(this);

        framework.data.observe(resources, this.observeCallback, contexts);

    };

    TimeBar.prototype = new framework.widgets.CommonWidget(true);

    TimeBar.prototype.updateData = function(framework_data) {

        var data = getNormalizedData(framework_data);

        // Remove previous container
        this.element.find(".timebar-container").remove();

        // Add new bar
        this.element.append('

');
        var progress = this.element.find(".progress");

        // Select the color function (an user given function or an object based function)
        var colorFunction = null;
        if(typeof this.configuration.color === 'function') {
            colorFunction = this.configuration.color;
        } else {
            colorFunction = function(val) {
                return this.configuration.color[String(val)];
            }.bind(this);
        }

        var width = 100 / data.values.length;
        var timeIncremental = (data.interval.to - data.interval.from) / data.values.length;

        // Create all the chunks of the bar
        for(var i = 0; i < data.values.length; ++i) {
            var color = colorFunction(data.values[i]);
            var time = data.interval.from + i * timeIncremental;
            progress.append($('

')
                    .tooltip({
                        html: true
                    })
            );
        }

        //Add labels
        var container = this.element.find(".timebar-container");
        var timeFormat = d3.time.format("%Y-%m-%d");
        var xAxis = $('

')
            .append($('' + timeFormat(new Date(data.interval.from)) + '')
                .css('position', 'absolute')
                .css('margin', '5px')
                .css('left', 0)
            )
            .append($('' + timeFormat(new Date(data.interval.to)) + '')
                .css('position', 'absolute')
                .css('margin', '5px')
                .css('right', 0)
            );
        container.append(xAxis);

    };

    TimeBar.prototype.delete = function() {

        // Stop observing for data changes
        framework.data.stopObserve(this.observeCallback);

        this.element.remove(".progress");

    };

    var getNormalizedData = function getNormalizedData(framework_data) {

        /* //TODO: uncomment for real data
        var resourceId = Object.keys(framework_data)[0];
        var resourceUID = Object.keys(framework_data[resourceId])[0];
        var resource = framework_data[resourceId][resourceUID];

        return {
            values: resource['data']['values'],
            interval: resource['data']['interval']
        };
        */

        var values = [0];
        var changeDiff = 0.01;
        var change = changeDiff;
        for(var i = 1; i < 1000; i++) {

            if(change === 0 && Math.random() > 0.95) {
                if(values[i-1] === 0) {
                    change = changeDiff;
                } else {
                    change = -changeDiff;
                }
            }

            values[i] = Math.max(0, Math.min(values[i-1] + change, 1));
            if(values[i] === 0 || values[i] === 1) {
                change = 0;
            }
        }

        return {
            values: values,
            interval: {
                from: 1445327267726,
                to: 1445427267726
            }
        };

    };

    // Register the widget in the framework
    window.framework.widgets.TimeBar = TimeBar;

    // AMD compliant
    if ( typeof define === "function" && define.amd) {
        define( [ /* List of dependencies */ ], function () { return TimeBar; } );
    }

})();<commit_msg>[Widget][TimeBar] Improved tooltip using qTip.<commit_after>/*
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      This file is part of the Smart Developer Hub Project:
        http://www.smartdeveloperhub.org/
      Center for Open Middleware
            http://www.centeropenmiddleware.com/
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      Copyright (C) 2015 Center for Open Middleware.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      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
                http://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.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
*/

(function() {

    var defaultConfig = {
        height: {
            type: ['number'],
            default: 100
        },
        color: {
            type: ['function', 'object'],
            default: function() { return "#000000"; }
        },
        tooltip: {
            type: ['string', 'function'],
            default: null
        },
        legend: {
            type: [Array],
            default: []
        }
    };

    /* TimeBar constructor
     *   element: the DOM element that will contain the widget
     *   resources: resources to observe
     *   contexts: list of contexts
     *   configuration: additional chart configuration:
     *      {
     *       ~ height: number - Height of the widget.
     *       ~ color: function - Function to decide the color of the point.
     *       ~ tooltip: string - Format string for the tooltip. Metric parameters can be used as variables by
     *         surrounding their names with the symbol '¬'. Metrics data can be accessed with _D. For example,
     *         the following is a valid labelFormat: "Repository: ¬_D.repocommits.info.rid.name¬".
     *         You can also access extra elements like ¬_E.value¬ and ¬_E.time¬
     *      }
     */
    var TimeBar = function TimeBar(element, resources, contexts, configuration) {

        this.element = $(element); //Store as jquery object

        // Extending widget
        framework.widgets.CommonWidget.call(this, false, element);

        // Configuration
        this.configuration = this.normalizeConfig(defaultConfig, configuration);

        // Use the callback offered by widget.common
        this.observeCallback = this.commonObserveCallback.bind(this);

        framework.data.observe(resources, this.observeCallback, contexts);

    };

    TimeBar.prototype = new framework.widgets.CommonWidget(true);

    TimeBar.prototype.updateData = function(framework_data) {

        var normalizedData = getNormalizedData.call(this,framework_data);

        paint.call(this, normalizedData, framework_data);

    };

    TimeBar.prototype.delete = function() {

        // Stop observing for data changes
        framework.data.stopObserve(this.observeCallback);

        this.element.remove(".progress");

    };

    var getNormalizedData = function getNormalizedData(framework_data) {

        /* //TODO: uncomment for real data
        var resourceId = Object.keys(framework_data)[0];
        var resourceUID = Object.keys(framework_data[resourceId])[0];
        var resource = framework_data[resourceId][resourceUID];

        return {
            values: resource['data']['values'],
            interval: resource['data']['interval']
        };
        */

        /*var values = [0];
        var changeDiff = 0.01;
        var change = changeDiff;
        for(var i = 1; i < 1000; i++) {

            if(change === 0 && Math.random() > 0.95) {
                if(values[i-1] === 0) {
                    change = changeDiff;
                } else {
                    change = -changeDiff;
                }
            }

            values[i] = Math.max(0, Math.min(values[i-1] + change, 1));
            if(values[i] === 0 || values[i] === 1) {
                change = 0;
            }
        }

        return {
            values: values,
            interval: {
                from: 1445327267726,
                to: 1445427267726
            }
        };*/

        return {
            values: [0.2, 0.2, 0.2, 1, 0.5, 0.5, 0.7, 0.7, 0.7],
            interval: {
                from: 1445327267726,
                to: 1445427267726
            }
        };

    };

    var paint = function(data, framework_data) {

        // Remove previous container
        this.element.find(".timebar-container").remove();

        // Add new bar
        this.element.append('

');
        var progress = this.element.find(".progress");

        // Select the color function (an user given function or an object based function)
        var colorFunction = null;
        if(typeof this.configuration.color === 'function') {
            colorFunction = this.configuration.color;
        } else {
            colorFunction = function(val) {
                return this.configuration.color[String(val)];
            }.bind(this);
        }

        var width = 100 / data.values.length;
        var timeIncremental = (data.interval.to - data.interval.from) / data.values.length;

        // Create all the chunks of the bar
        for(var i = 0; i < data.values.length; ++i) {
            var color = colorFunction(data.values[i]);
            var time = data.interval.from + i * timeIncremental;
            var content = null;

            //Configure how the tooltip looks.
            if(typeof this.configuration.tooltip === 'string') {
                content = this.replace(this.configuration.tooltip, framework_data, { value: data.values[i], time: time } );
            } else if(typeof this.configuration.tooltip === 'function') {
                content = this.configuration.tooltip( { value: data.values[i], time: time } );
            }

            addBarToProgressBar(progress, width, color, content);

        }

        //Add labels
        var container = this.element.find(".timebar-container");
        var timeFormat = d3.time.format("%Y-%m-%d");
        var xAxis = $('

')
            .append($('' + timeFormat(new Date(data.interval.from)) + '')
                .css('position', 'absolute')
                .css('margin', '5px')
                .css('left', 0)
        )
            .append($('' + timeFormat(new Date(data.interval.to)) + '')
                .css('position', 'absolute')
                .css('margin', '5px')
                .css('right', 0)
        );
        container.append(xAxis);

    };

    var addBarToProgressBar = function (progress, width, color, content) {
        var bar = $('

')
            .qtip({
                content: function() {
                    return content;
                },
                show: {
                    event: 'mouseover'
                },
                hide: {
                    event: 'mouseout'
                },
                position: {
                    my: 'top center',
                    at: 'bottom center'
                },
                style: {
                    classes: 'qtip-bootstrap',
                    tip: {
                        width: 16,
                        height: 6
                    }
                }
            });

        // Stop observing for data changes
        framework.data.stopObserve(this.observeCallback);

        this.element.remove(".progress");

    };

    var getNormalizedData = function getNormalizedData(framework_data) {

        /* //TODO: uncomment for real data
        var resourceId = Object.keys(framework_data)[0];
        var resourceUID = Object.keys(framework_data[resourceId])[0];
        var resource = framework_data[resourceId][resourceUID];

        return {
            values: resource['data']['values'],
            interval: resource['data']['interval']
        };
        */

        var values = [0];
        var changeDiff = 0.01;
        var change = changeDiff;
        for(var i = 1; i < 1000; i++) {

            if(change === 0 && Math.random() > 0.95) {
                if(values[i-1] === 0) {
                    change = changeDiff;
                } else {
                    change = -changeDiff;
                }
            }

            values[i] = Math.max(0, Math.min(values[i-1] + change, 1));
            if(values[i] === 0 || values[i] === 1) {
                change = 0;
            }
        }

        return {
            values: values,
            interval: {
                from: 1445327267726,
                to: 1445427267726
            }
        progress.append(bar);

    };

    // Register the widget in the framework
    window.framework.widgets.TimeBar = TimeBar;

    // AMD compliant
    if ( typeof define === "function" && define.amd) {
        define( [ 'jquery-qtip' ], function () { return TimeBar; } );
    }

})();<|endoftext|>


<commit_before>/* eslint-disable jsx-a11y/label-has-for*/
import React from 'react';
import Immutable from 'immutable';
import moment from 'moment';
import _ from 'lodash';

import TimeInput from '../../../TimeInput';
import * as periodUtils from '../../../../../common/periodUtils';

import styles from './less/PeriodsForm.less';

function findType(types, value) {
    return types.find(type => type.get('pty_id') === value);
}

export default class extends React.Component {
    static propTypes = {
        period: React.PropTypes.instanceOf(Immutable.Map).isRequired,
        types: React.PropTypes.instanceOf(Immutable.List).isRequired,
        dayTargetTime: React.PropTypes.objectOf(moment.duration).isRequired,
        index: React.PropTypes.number.isRequired,
        onRemove: React.PropTypes.func.isRequired,
        onUpdate: React.PropTypes.func.isRequired,
    };

    constructor(props, context) {
        super(props, context);

        let period = props.period;
        if (!period.get('type')) {
            period = period.merge({
                type: findType(props.types, 'Work'),
            });
        }
        // handle default duration to be the first duration found in the config
        if (!period.get('duration')) {
            period = period.merge({
                // eslint-disable-next-line newline-per-chained-call
                duration: period.get('type').get('pty_config').get('types').keySeq().first(),
            });
            // add duration per_duration object if needed
            period = period.merge(this.addDurationTime(period));
        }

        this.state = { period };

        this.handleComment = this.handleComment.bind(this);
        this.handleDurationChange = this.handleDurationChange.bind(this);
        this.handleTimeChange = this.handleTimeChange.bind(this);
        this.handleTypeChange = this.handleTypeChange.bind(this);
        this.renderDurationRadio = this.renderDurationRadio.bind(this);
        this.renderSelectOption = this.renderSelectOption.bind(this);
    }

    componentDidMount() {
        // focus on select on first creation
        this.selectType.focus();
    }

    handleTypeChange(event) {
        const { period } = this.state;
        const { types } = this.props;

        const type = findType(types, event.target.value);
        const cfg = type.get('pty_config').get('types').toJS();
        const defaultDuration = _.findKey(cfg, value => value) || periodUtils.NONE;
        const durationValue = period.get('duration');

        // cleanup
        let breakDuration = period.get('per_break');
        let start = period.get('per_start');
        let stop = period.get('per_stop');
        if (!cfg.period) {
            breakDuration = null;
            start = null;
            stop = null;
        }

        const p = period.merge({
            type,
            per_start: start,
            per_stop: stop,
            per_break: breakDuration,
            duration: cfg[durationValue] ? durationValue : defaultDuration,
        });
        this.updateState(p);
    }

    handleDurationChange(event) {
        let period = this.state.period.merge({
            duration: event.target.value,
        });

        // if duration type is not period, remove it
        if (event.target.value !== periodUtils.PERIOD) {
            period = period.merge({
                per_start: null,
                per_stop: null,
                per_break: null,
            });
        }

        this.updateState(period);
    }

    handleTimeChange(name, duration) {
        const value = {};
        value[name] = duration;
        const period = this.state.period.merge(value);

        this.updateState(period);
    }

    handleComment(event) {
        const period = this.state.period.merge({
            per_comment: event.target.value,
        });
        this.updateState(period);
    }

    addDurationTime(period) {
        return {
            per_duration: periodUtils.calculateDuration(period, this.props.dayTargetTime),
        };
    }

    updateState(period) {
        // add duration per_duration object if needed
        const updatedPeriod = period.merge(this.addDurationTime(period));
        this.props.onUpdate(updatedPeriod);
        this.setState({ period: updatedPeriod });
    }

    renderSelectOption(type) {
        return (
            <option value={type.get('pty_id')} key={type.get('pty_id')}>{type.get('pty_name')}</option>
        );
    }

    renderDurationRadio(elementName, value, duration, index) {
        const id = `${elementName}-${duration.name}`;

        return (

                    <input
                        type="radio" name={`${elementName}[duration]`} id={id} value={duration.name}
                        checked={duration.name === value}
                        onChange={this.handleDurationChange}
                    />
                    {duration.description}

        );
    }

    renderErrorMessages() {
        const period = this.state.period;
        const errors = periodUtils.getAllErrors(period);

        return (

                {errors.map((msg, index) => {msg})}

        );
    }

    render() {
        const period = this.state.period;
        const isValid = periodUtils.validatePeriod(period);

        const elementName = `period-${period.get('per_id') ? period.get('per_id') : this.props.index}`;

        const cfg = period.getIn(['type', 'pty_config', 'types']) || new Immutable.Map();

        const durations = periodUtils.durationConfig.filter(d => cfg.get(d.name) === true);

        let periodElements = [];
        const elementCss = 'controls col-xs-6 col-sm-2 col-lg-1 tt-col-lg-1';
        const comment = { name: `${elementName}[per_comment]`, className: 'controls col-xs-12 col-lg-8' };
        if (period.get('duration') === periodUtils.PERIOD) {
            periodElements = [{
                id: 'per_start',
                label: 'Startzeit',
                name: `${elementName}[per_start]`,
                className: elementCss,
                placeholder: 'hh:mm',
                value: period.get('per_start'),
                required: true,
                round: -15,
            }, {
                id: 'per_stop',
                label: 'Endzeit',
                name: `${elementName}[per_stop]`,
                className: elementCss,
                placeholder: 'hh:mm',
                value: period.get('per_stop'),
                required: false,
                round: +15,
            }, {
                id: 'per_break',
                label: 'Pause',
                name: `${elementName}[per_break]`,
                className: elementCss,
                placeholder: 'hh:mm',
                value: period.get('per_break'),
                required: false,
                round: +15,
            }];

            comment.className += ' col-sm-6';
        } else if (period.get('duration') === periodUtils.DURATION) {
            periodElements = [{
                id: 'per_duration',
                label: 'Zeit',
                name: `${elementName}[per_duration]`,
                className: elementCss,
                placeholder: 'hh:mm',
                value: period.get('per_duration'),
                required: true,
                round: null,
                negative: true,
            }];
        }

        const ref = (selectType) => {
            this.selectType = selectType;
        };

        return (

                {!isValid ? this.renderErrorMessages() : ''}

                        <select
                            className="col-xs-12 form-control"
                            name={`${elementName}[pty_id]`}
                            value={period.getIn(['type', 'pty_id'])}
                            onChange={this.handleTypeChange}
                            ref={ref}
                        >
                            {this.props.types.toList().map(this.renderSelectOption)}
                        </select>

                    {durations.map(this.renderDurationRadio.bind(null, elementName, period.get('duration')))}

                    {periodElements.map((p, index) => (
                        <TimeInput
                            id={p.id}
                            label={p.label}
                            name={p.name}
                            css={p.className}
                            placeholder={p.placeholder}
                            time={p.value}
                            required={p.required}
                            key={index}
                            round={p.round}
                            allowNegativeValues={p.negative || false}
                            onChange={this.handleTimeChange}
                        />
                    ))}

                        Kommentar
                        <input
                            type="text" placeholder="Kommentar" className="form-control"
                            name={comment.name} id={comment.name} value={period.get('per_comment') || ''}
                            onChange={this.handleComment}
                        />

        );
    }
}
<commit_msg>fix switching type still showed per_start<commit_after>/* eslint-disable jsx-a11y/label-has-for*/
import React from 'react';
import Immutable from 'immutable';
import moment from 'moment';
import _ from 'lodash';

import TimeInput from '../../../TimeInput';
import * as periodUtils from '../../../../../common/periodUtils';

import styles from './less/PeriodsForm.less';

function findType(types, value) {
    return types.find(type => type.get('pty_id') === value);
}

export default class extends React.Component {
    static propTypes = {
        period: React.PropTypes.instanceOf(Immutable.Map).isRequired,
        types: React.PropTypes.instanceOf(Immutable.List).isRequired,
        dayTargetTime: React.PropTypes.objectOf(moment.duration).isRequired,
        index: React.PropTypes.number.isRequired,
        onRemove: React.PropTypes.func.isRequired,
        onUpdate: React.PropTypes.func.isRequired,
    };

    constructor(props, context) {
        super(props, context);

        let period = props.period;
        if (!period.get('type')) {
            period = period.merge({
                type: findType(props.types, 'Work'),
            });
        }
        // handle default duration to be the first duration found in the config
        if (!period.get('duration')) {
            period = period.merge({
                // eslint-disable-next-line newline-per-chained-call
                duration: period.get('type').get('pty_config').get('types').keySeq().first(),
            });
            // add duration per_duration object if needed
            period = period.merge(this.addDurationTime(period));
        }

        this.state = { period };

        this.handleComment = this.handleComment.bind(this);
        this.handleDurationChange = this.handleDurationChange.bind(this);
        this.handleTimeChange = this.handleTimeChange.bind(this);
        this.handleTypeChange = this.handleTypeChange.bind(this);
        this.renderDurationRadio = this.renderDurationRadio.bind(this);
        this.renderSelectOption = this.renderSelectOption.bind(this);
    }

    componentDidMount() {
        // focus on select on first creation
        this.selectType.focus();
    }

    handleTypeChange(event) {
        const { period } = this.state;
        const { types } = this.props;

        const type = findType(types, event.target.value);
        const cfg = type.get('pty_config').get('types').toJS();
        const defaultDuration = _.findKey(cfg, value => value) || periodUtils.NONE;
        const durationValue = period.get('duration');

        // cleanup
        let breakDuration = period.get('per_break');
        let start = period.get('per_start');
        let stop = period.get('per_stop');
        if (!cfg.period) {
            breakDuration = null;
            start = null;
            stop = null;
        }

        const p = period.merge({
            type,
            per_start: start,
            per_stop: stop,
            per_break: breakDuration,
            duration: cfg[durationValue] ? durationValue : defaultDuration,
        });
        this.updateState(p);
    }

    handleDurationChange(event) {
        let period = this.state.period.merge({
            duration: event.target.value,
        });

        // if duration type is not period, remove it
        if (event.target.value !== periodUtils.PERIOD) {
            period = period.merge({
                per_start: null,
                per_stop: null,
                per_break: null,
            });
        }

        this.updateState(period);
    }

    handleTimeChange(name, duration) {
        const value = {};
        value[name] = duration;
        const period = this.state.period.merge(value);

        this.updateState(period);
    }

    handleComment(event) {
        const period = this.state.period.merge({
            per_comment: event.target.value,
        });
        this.updateState(period);
    }

    addDurationTime(period) {
        return {
            per_duration: periodUtils.calculateDuration(period, this.props.dayTargetTime),
        };
    }

    updateState(period) {
        // add duration per_duration object if needed
        const updatedPeriod = period.merge(this.addDurationTime(period));
        this.props.onUpdate(updatedPeriod);
        this.setState({ period: updatedPeriod });
    }

    renderSelectOption(type) {
        return (
            <option value={type.get('pty_id')} key={type.get('pty_id')}>{type.get('pty_name')}</option>
        );
    }

    renderDurationRadio(elementName, value, duration, index) {
        const id = `${elementName}-${duration.name}`;

        return (

                    <input
                        type="radio" name={`${elementName}[duration]`} id={id} value={duration.name}
                        checked={duration.name === value}
                        onChange={this.handleDurationChange}
                    />
                    {duration.description}

        );
    }

    renderErrorMessages() {
        const period = this.state.period;
        const errors = periodUtils.getAllErrors(period);

        return (

                {errors.map((msg, index) => {msg})}

        );
    }

    render() {
        const period = this.state.period;
        const isValid = periodUtils.validatePeriod(period);

        const elementName = `period-${period.get('per_id') ? period.get('per_id') : this.props.index}`;

        const cfg = period.getIn(['type', 'pty_config', 'types']) || new Immutable.Map();

        const durations = periodUtils.durationConfig.filter(d => cfg.get(d.name) === true);

        let periodElements = [];
        const elementCss = 'controls col-xs-6 col-sm-2 col-lg-1 tt-col-lg-1';
        const comment = { name: `${elementName}[per_comment]`, className: 'controls col-xs-12 col-lg-8' };
        if (period.get('duration') === periodUtils.PERIOD) {
            periodElements = [{
                id: 'per_start',
                label: 'Startzeit',
                name: `${elementName}[per_start]`,
                className: elementCss,
                placeholder: 'hh:mm',
                value: period.get('per_start'),
                required: true,
                round: -15,
            }, {
                id: 'per_stop',
                label: 'Endzeit',
                name: `${elementName}[per_stop]`,
                className: elementCss,
                placeholder: 'hh:mm',
                value: period.get('per_stop'),
                required: false,
                round: +15,
            }, {
                id: 'per_break',
                label: 'Pause',
                name: `${elementName}[per_break]`,
                className: elementCss,
                placeholder: 'hh:mm',
                value: period.get('per_break'),
                required: false,
                round: +15,
            }];

            comment.className += ' col-sm-6';
        } else if (period.get('duration') === periodUtils.DURATION) {
            periodElements = [{
                id: 'per_duration',
                label: 'Zeit',
                name: `${elementName}[per_duration]`,
                className: elementCss,
                placeholder: 'hh:mm',
                value: period.get('per_duration'),
                required: true,
                round: null,
                negative: true,
            }];
        }

        const ref = (selectType) => {
            this.selectType = selectType;
        };

        return (

                {!isValid ? this.renderErrorMessages() : ''}

                        <select
                            className="col-xs-12 form-control"
                            name={`${elementName}[pty_id]`}
                            value={period.getIn(['type', 'pty_id'])}
                            onChange={this.handleTypeChange}
                            ref={ref}
                        >
                            {this.props.types.toList().map(this.renderSelectOption)}
                        </select>

                    {durations.map(this.renderDurationRadio.bind(null, elementName, period.get('duration')))}

                    {periodElements.map((p, index) => (
                        <TimeInput
                            id={p.id}
                            label={p.label}
                            name={p.name}
                            css={p.className}
                            placeholder={p.placeholder}
                            time={p.value}
                            required={p.required}
                            key={p.id}
                            round={p.round}
                            allowNegativeValues={p.negative || false}
                            onChange={this.handleTimeChange}
                        />
                    ))}

                        Kommentar
                        <input
                            type="text" placeholder="Kommentar" className="form-control"
                            name={comment.name} id={comment.name} value={period.get('per_comment') || ''}
                            onChange={this.handleComment}
                        />

        );
    }
}
<|endoftext|>