[Fusionforge-commits] FusionForge branch master updated. 6.0.4-1230-gfd5f1ed

Franck Villaume nerville at libremir.placard.fr.eu.org
Wed Dec 7 09:49:12 CET 2016


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "FusionForge".

The branch, master has been updated
       via  fd5f1ed29a2c53118760c4023a96504802eb163a (commit)
       via  c6eeda5d1a751b37bdf1c8a1164704270eb766c4 (commit)
       via  1b1e9ead01a76654dbf3cd01e7e9cd385b8044a7 (commit)
      from  dec530c082e588f3a52720a47df618a0855b50d6 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
https://scm.fusionforge.org/anonscm/gitweb/?p=fusionforge/fusionforge.git;a=commitdiff;h=fd5f1ed29a2c53118760c4023a96504802eb163a

commit fd5f1ed29a2c53118760c4023a96504802eb163a
Merge: dec530c c6eeda5
Author: Franck Villaume <franck.villaume at trivialdev.com>
Date:   Wed Dec 7 09:48:51 2016 +0100

    Merge remote-tracking branch 'bredt/master'


https://scm.fusionforge.org/anonscm/gitweb/?p=fusionforge/fusionforge.git;a=commitdiff;h=c6eeda5d1a751b37bdf1c8a1164704270eb766c4

commit c6eeda5d1a751b37bdf1c8a1164704270eb766c4
Author: Stéphane-Eymeric Bredthauer <sebredthauer at gmail.com>
Date:   Mon Dec 5 16:59:50 2016 +0100

    Tracker: schema for Formula to calculate field values

diff --git a/src/db/20161128-tracker-field-formula.sql b/src/db/20161128-tracker-field-formula.sql
new file mode 100644
index 0000000..89ff3de
--- /dev/null
+++ b/src/db/20161128-tracker-field-formula.sql
@@ -0,0 +1,10 @@
+CREATE TABLE artifact_extra_field_formula (
+  formula_id serial NOT NULL,
+  extra_field_id integer NOT NULL,
+  id integer DEFAULT NULL,
+  formula text NOT NULL,
+  CONSTRAINT artifact_extra_field_formula_pkey PRIMARY KEY (formula_id),
+  CONSTRAINT artifact_extra_field_formula_extra_field_id_fkey FOREIGN KEY (extra_field_id)
+      REFERENCES artifact_extra_field_list (extra_field_id) MATCH SIMPLE
+      ON UPDATE NO ACTION ON DELETE CASCADE
+);

https://scm.fusionforge.org/anonscm/gitweb/?p=fusionforge/fusionforge.git;a=commitdiff;h=1b1e9ead01a76654dbf3cd01e7e9cd385b8044a7

commit 1b1e9ead01a76654dbf3cd01e7e9cd385b8044a7
Author: Stéphane-Eymeric Bredthauer <sebredthauer at gmail.com>
Date:   Mon Dec 5 10:56:19 2016 +0100

    Tracker: Formula to calculate field values

diff --git a/src/common/tracker/ArtifactExtraField.class.php b/src/common/tracker/ArtifactExtraField.class.php
index ae5478f..9d552f6 100644
--- a/src/common/tracker/ArtifactExtraField.class.php
+++ b/src/common/tracker/ArtifactExtraField.class.php
@@ -663,6 +663,80 @@ class ArtifactExtraField extends FFError {
 	}
 
 	/**
+	 * getFormula - Get formula(s) to calculate field
+	 *
+	 * @return	string|array
+	 */
+	function getFormula() {
+		$return = false;
+		$res = db_query_params ('SELECT id, formula FROM artifact_extra_field_formula WHERE extra_field_id=$1',
+				array ($this->getID()));
+		$type = $this->getType();
+		if (in_array($type, unserialize(ARTIFACT_EXTRAFIELDTYPEGROUP_VALUE))) {
+			if (db_numrows($res) > 0) {
+				$row = db_fetch_array($res);
+				$return = $row['formula'];
+			} else {
+				$return ='';
+			}
+		} elseif (in_array($type, array_merge(unserialize(ARTIFACT_EXTRAFIELDTYPEGROUP_SPECALCHOICE), unserialize(ARTIFACT_EXTRAFIELDTYPEGROUP_CHOICE)))) {
+			$return = array();
+			while ($row = db_fetch_array($res)) {
+				$return[$row['id']] = $row['formula'];
+			}
+		}
+		return $return;
+	}
+
+	/**
+	 * setFormula - set formula to calculate field value
+	 *
+	 * @param	string	$formula	formula
+	 * @return	string|array
+	 */
+	function setFormula($formula) {
+		$formula = $trim($formula);
+		$return = true;
+		if ($formula=='') {
+			$this->resetFormula();
+		} else {
+			$type = $this->getType();
+			$efID = $this->getID();
+			$res = db_query_params ('SELECT id, formula FROM artifact_extra_field_formula WHERE extra_field_id=$1',
+					array ($efID)) ;
+			if (db_numrows($res) > 0) {
+				$res = db_query_params ('UPDATE artifact_extra_field_formula SET formula = $1 WHERE extra_field_id=$2',
+						array ($formula, $efID));
+			} else {
+				$res = db_query_params ('INSERT INTO artifact_extra_field_formula (extra_field_id, formula) VALUES ($1,$2)',
+						array ($efID, $formula)) ;
+			}
+			if (!$res) {
+				$this->setError(db_error());
+				$return = false;
+			}
+		}
+		return $return;
+	}
+
+	/**
+	 * resetFormula - reset formula
+	 *
+	 * @return	boolean
+	 */
+	function resetFormula() {
+		$result = db_query_params ('DELETE FROM artifact_extra_field_formula WHERE extra_field_id = $1',
+				array ($this->getID()));
+		if (!$result) {
+			$this->setError(db_error());
+			$return = false;
+		} else {
+			$return = true;
+		}
+		return $return;
+	}
+
+	/**
 	 * getAvailableTypes - the types of text fields and their names available.
 	 *
 	 * @return	array	types.
diff --git a/src/common/tracker/ArtifactExtraFieldElement.class.php b/src/common/tracker/ArtifactExtraFieldElement.class.php
index 2a5e8aa..382bebf 100644
--- a/src/common/tracker/ArtifactExtraFieldElement.class.php
+++ b/src/common/tracker/ArtifactExtraFieldElement.class.php
@@ -346,6 +346,61 @@ class ArtifactExtraFieldElement extends FFError {
 		}
 	}
 
+	function getFormula() {
+		$return = false;
+		$res = db_query_params ('SELECT formula FROM artifact_extra_field_formula WHERE extra_field_id=$1 AND id=$2',
+				array ($this->ArtifactExtraField->getID(),
+					$this->getID()));
+		if (db_numrows($res) > 0) {
+			$row = db_fetch_array($res);
+			$return = $row['formula'];
+		} else {
+			$return ='';
+		}
+		return $return;
+	}
+
+	function setFormula($formula) {
+		$formula = trim($formula);
+		$return = true;
+		if ($formula=='') {
+			$this->resetFormula();
+		} else {
+			$res = db_query_params ('SELECT id, formula FROM artifact_extra_field_formula WHERE extra_field_id=$1 and id=$2',
+				array ($this->ArtifactExtraField->getID(),
+					$this->getID()));
+			if (db_numrows($res) > 0) {
+				$res = db_query_params ('UPDATE artifact_extra_field_formula SET formula = $1 WHERE extra_field_id=$2 and id=$3',
+					array ($formula,
+						$this->ArtifactExtraField->getID(),
+						$this->getID()));
+			} else {
+				$res = db_query_params ('INSERT INTO artifact_extra_field_formula (extra_field_id, id, formula) VALUES ($1,$2,$3)',
+					array ($this->ArtifactExtraField->getID(),
+						$this->getID(),
+						$formula));
+			}
+			if (!$res) {
+				$this->setError(db_error());
+				$return = false;
+			}
+		}
+		return $return;
+	}
+
+	function resetFormula() {
+		$result = db_query_params ('DELETE FROM artifact_extra_field_formula WHERE extra_field_id=$1 and id=$2',
+				array ($this->ArtifactExtraField->getID(),
+					$this->getID()));
+		if (!$result) {
+			$this->setError(db_error());
+			$return = false;
+		} else {
+			$return = true;
+		}
+		return $return;
+	}
+
 	/**
 	 * getChildrenElements - return the array of the elements of children fields who depend on current element
 	 *
diff --git a/src/common/tracker/ArtifactType.class.php b/src/common/tracker/ArtifactType.class.php
index d5ee5ba..107c974 100644
--- a/src/common/tracker/ArtifactType.class.php
+++ b/src/common/tracker/ArtifactType.class.php
@@ -694,6 +694,51 @@ class ArtifactType extends FFError {
 	}
 
 	/**
+	 * getExtraFieldsInFormula - Get array of extra fields used in formula
+	 *
+	 * @param	array	$types
+	 * @param	bool	$get_is_disabled
+	 * @param	bool	$get_is_hidden_on_submit
+	 * @return	array	arrays of data;
+	 */
+	function getExtraFieldsInFormula($types = array(), $get_is_disabled = false, $get_is_hidden_on_submit = true) {
+		$return = array();
+		$extra_fields = $this->getExtraFields($types, $get_is_disabled, $get_is_hidden_on_submit);
+		$res = db_query_params('SELECT string_agg(formula,chr(10)) FROM artifact_extra_field_formula NATURAL INNER JOIN artifact_extra_field_list WHERE is_disabled=0 AND group_artifact_id=$1',
+				array ($this->getID()));
+		if (db_numrows($res) > 0) {
+			$row = db_fetch_array($res);
+			if (preg_match_all("/([a-z]\w*)/m", $row[0], $matches)) {
+				foreach ($extra_fields as $extra_field) {
+					if (in_array($extra_field['alias'],$matches[0])) {
+						$return[]=$extra_field['extra_field_id'];
+					}
+				}
+			}
+		}
+		return $return;
+	}
+
+	/**
+	 * getExtraFieldsWithFormula - Get array of extra fields with formula
+	 *
+	 * @param	array	$types
+	 * @param	bool	$get_is_disabled
+	 * @param	bool	$get_is_hidden_on_submit
+	 * @return	array	arrays of data;
+	 */
+	function getExtraFieldsWithFormula($types = array(), $get_is_disabled = false, $get_is_hidden_on_submit = true) {
+		$return = array();
+		$extra_fields = $this->getExtraFields($types, $get_is_disabled, $get_is_hidden_on_submit);
+		$res = db_query_params('SELECT extra_field_id FROM artifact_extra_field_formula NATURAL INNER JOIN artifact_extra_field_list WHERE is_disabled=0 AND group_artifact_id=$1',
+				array ($this->getID()));
+		while ($arr = db_fetch_array($res)) {
+			$return []= $arr['extra_field_id'];
+		}
+		return $return;
+	}
+
+	/**
 	 * cloneFieldsFrom - clone all the fields and elements from another tracker
 	 *
 	 * @param	int	$clone_tracker_id	id of the cloned tracker
diff --git a/src/common/tracker/actions/admin-updates.php b/src/common/tracker/actions/admin-updates.php
index 445c0bc..5962d8f 100644
--- a/src/common/tracker/actions/admin-updates.php
+++ b/src/common/tracker/actions/admin-updates.php
@@ -246,6 +246,7 @@ if (getStringFromRequest('add_extrafield')) {
 	$is_hidden_on_submit = getStringFromRequest('is_hidden_on_submit');
 	$is_disabled = getStringFromRequest('is_disabled');
 	$defaultArr = getArrayFromRequest('extra_fields', false);
+	$formula = getStringFromRequest('formula');
 	if (isset($defaultArr[$id])) {
 		$default = $defaultArr[$id];
 	} else {
@@ -266,16 +267,20 @@ if (getStringFromRequest('add_extrafield')) {
 		if (!$ac->update($name, $attribute1, $attribute2, $is_required, $alias, $show100, $show100label, $description, $pattern, $parent, $autoassign, $is_hidden_on_submit, $is_disabled)) {
 			$error_msg .= _('Update failed')._(': ').$ac->getErrorMessage();
 			$ac->clearError();
-		} else {
-			if($default!==false) {
+		} elseif (!$ac->setFormula($formula)){
+				$error_msg .= _('Update field formula failed')._(': ').$ac->getErrorMessage();
+				$ac->clearError();
+		} elseif ($default!==false) {
 				if (!$ac->setDefaultValues($default)){
-					$error_msg .= _('Update failed')._(': ').$ac->getErrorMessage();
+					$error_msg .= _('Update field default value failed')._(': ').$ac->getErrorMessage();
 					$ac->clearError();
 				} else {
 					$feedback .= _('Custom Field updated');
 					$next = 'add_extrafield';
 				}
-			}
+		} else {
+			$feedback .= _('Custom Field updated');
+			$next = 'add_extrafield';
 		}
 	}
 
@@ -285,6 +290,7 @@ if (getStringFromRequest('add_extrafield')) {
 } elseif (getStringFromRequest('update_opt')) {
 	$boxid = getStringFromRequest('boxid');
 	$is_default = getStringFromRequest('is_default', false);
+	$formula = getStringFromRequest('formula', '');
 	$ac = new ArtifactExtraField($ath,$boxid);
 	if (!$ac || !is_object($ac)) {
 		$error_msg .= _('Unable to create ArtifactExtraField Object');
@@ -311,8 +317,14 @@ if (getStringFromRequest('add_extrafield')) {
 					$error_msg .= _('Update failed')._(': ').$ao->getErrorMessage();
 					$ao->clearError();
 				} else {
-				$feedback .= html_e('br')._('Parent Elements updated');
-				$next = 'add_extrafield';
+					$feedback .= html_e('br')._('Parent Elements updated');
+					if (!$ao->setFormula($formula)) {
+						$error_msg .= _('Update failed')._(': ').$ao->getErrorMessage();
+						$ao->clearError();
+					} else {
+						$feedback .= html_e('br')._('Formula updated');
+						$next = 'add_extrafield';
+					}
 				}
 			}
 		}
diff --git a/src/common/tracker/actions/ajax.php b/src/common/tracker/actions/ajax.php
index 5397d9d..b916e4c 100644
--- a/src/common/tracker/actions/ajax.php
+++ b/src/common/tracker/actions/ajax.php
@@ -3,6 +3,7 @@
  * Tracker Facility
  *
  * Copyright (C) 2011 Alain Peyrat - Alcatel-Lucent
+ * Copyright 2016 Stéphane-Eymeric Bredthauer - TrivialDev
  * http://fusionforge.org/
  *
  * This file is part of FusionForge. FusionForge is free software;
@@ -21,6 +22,11 @@
  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
 
+require_once $gfwww.'include/expression.php';
+
+global $group;
+global $atid;
+
 $sysdebug_enable = false;
 
 $function = getStringFromRequest('function');
@@ -30,6 +36,10 @@ switch ($function) {
 		$canned_response_id = getIntFromRequest('canned_response_id');
 		echo get_canned_response($canned_response_id);
 		break;
+	case 'get_formulas_results':
+		$extra_fields = getArrayFromRequest('extra_fields');
+		echo get_formulas_results($group, $atid, $extra_fields);
+		break;
 	default:
 		echo '';
 		break;
@@ -45,3 +55,74 @@ function get_canned_response($id) {
 		return db_result($result, 0, 'body');
 	}
 }
+
+function get_formulas_results($group, $atid, $extra_fields=array()){
+	$ret = array('messages' => '');
+	$at = new ArtifactType($group, $atid);
+	if (!$at || !is_object($at)) {
+		$ret['messages'] = _('ArtifactType could not be created');
+		return json_encode($ret);
+		exit();
+	}
+	if ($at->isError()) {
+		$ret['messages'] = $at->getErrorMessage();
+		return json_encode($ret);
+		exit();
+	}
+
+	$expr = new Expression();
+
+	// Variable assignment
+	$extraFields = $at->getExtraFields();
+	foreach ($extraFields as $extraField) {
+		if (isset($extra_fields[$extraField['extra_field_id']])) {
+			if ($extraField['field_type']==ARTIFACT_EXTRAFIELDTYPE_INTEGER) {
+				$varAss = $extraField['alias'].'='.$extra_fields[$extraField['extra_field_id']];
+				$expr->evaluate($varAss);
+			} elseif ($extraField['field_type']==ARTIFACT_EXTRAFIELDTYPE_TEXT) {
+				$varAss = $extraField['alias'].'="'.$extra_fields[$extraField['extra_field_id']].'"';
+				$expr->evaluate($varAss);
+			}
+		}
+	}
+
+	// formula
+	$result = array();
+	foreach ($extraFields as $extraField) {
+		$ef = new ArtifactExtraField($at,$extraField['extra_field_id']);
+		if (!$ef || !is_object($ef)) {
+			$ret['messages'] = _('ArtifactExtraField could not be created');
+			return json_encode($ret);
+			exit();
+		}
+		if ($ef->isError()) {
+			$ret['messages'] = $ef->getErrorMessage();
+			return json_encode($ret);
+			exit();
+		}
+		$formula = $ef->getFormula();
+		if (in_array($extraField['field_type'], unserialize(ARTIFACT_EXTRAFIELDTYPEGROUP_VALUE))) {
+			if (!empty($formula)) {
+				$value = $expr->evaluate($formula);
+				$result [] = array( 'id'=>$extraField['extra_field_id'], 'value'=>$value, 'error'=>$expr->last_error );
+			}
+		} elseif (in_array($extraField['field_type'], unserialize(ARTIFACT_EXTRAFIELDTYPEGROUP_CHOICE))) {
+			if (is_array($formula)) {
+				$formulas = $formula;
+				$valueArr = array();
+				foreach ($formulas as $key=>$formula) {
+					$value = $expr->evaluate($formula);
+					if ($value) {
+						$valueArr[]=$key;
+						if (in_array($extraField['field_type'], unserialize(ARTIFACT_EXTRAFIELDTYPEGROUP_SINGLECHOICE))) {
+							breack;
+						}
+					}
+				}
+				$result [] = array( 'id'=>$extraField['extra_field_id'], 'value'=>$valueArr, 'error'=>$expr->last_error);
+			}
+		}
+	}
+	$ret['fields'] = $result;
+	return json_encode($ret);
+}
diff --git a/src/common/tracker/include/ArtifactTypeHtml.class.php b/src/common/tracker/include/ArtifactTypeHtml.class.php
index bd2aacc..16f341f 100644
--- a/src/common/tracker/include/ArtifactTypeHtml.class.php
+++ b/src/common/tracker/include/ArtifactTypeHtml.class.php
@@ -255,6 +255,15 @@ class ArtifactTypeHtml extends ArtifactType {
 			echo $template;
 			return ;
 		}
+		if ($mode == 'UPDATE' || $mode == 'NEW') {
+			if ($mode == 'NEW') {
+				$efInFormula = $this->getExtraFieldsInFormula($types, false, false);
+				$efWithFormula = $this->getExtraFieldsWithFormula($types, false, false);
+			} else {
+				$efInFormula = $this->getExtraFieldsInFormula($types);
+				$efWithFormula = $this->getExtraFieldsWithFormula($types);
+			}
+		}
 
 		$keys = array_keys($efarr);
 		for ($k = 0; $k < count($keys); $k++) {
@@ -269,6 +278,16 @@ class ArtifactTypeHtml extends ArtifactType {
 				$attrs['required'] = 'required';
 			}
 
+			if ($mode == 'UPDATE' || $mode == 'NEW') {
+				if (in_array($efarr[$i]['extra_field_id'],$efInFormula)) {
+					$attrs['class'] = (empty($attrs['class']) ? '':$attrs['class'].' ').'in-formula';
+				}
+				if (in_array($efarr[$i]['extra_field_id'],$efWithFormula)) {
+					$attrs['class'] = (empty($attrs['class']) ? '':$attrs['class'].' ').'with-formula readonly';
+					$attrs['readonly'] = 'readonly';
+				}
+			}
+
 			if (!isset($selected[$efarr[$i]['extra_field_id']]))
 				$selected[$efarr[$i]['extra_field_id']] = '';
 
@@ -1034,7 +1053,9 @@ class ArtifactTypeHtml extends ArtifactType {
 	 * @return	string	HTML code of corresponding input tag.
 	 */
 	function renderIntegerField ($extra_field_id, $contents, $size, $maxlength, $attrs = array()) {
-		return html_e('input', array_merge(array('type'=>'number', 'name'=>'extra_fields['.$extra_field_id.']', 'value'=>$contents, 'size'=>$size, 'maxlength'=>$maxlength, 'min'=>0), $attrs));
+		$intAttrs = array('type'=>'number', 'name'=>'extra_fields['.$extra_field_id.']', 'value'=>$contents, 'size'=>$size, 'maxlength'=>$maxlength, 'min'=>0);
+		$newattrs =  array_merge($intAttrs, $attrs);
+		return html_e('input',$newattrs);
 	}
 
 	/**
@@ -1195,7 +1216,9 @@ class ArtifactTypeHtml extends ArtifactType {
 	function javascript() {
 		$jsvariable ="
 	var invalidSelectMsg = '"._("One or more of the selected options is not allowed")."';
-	var invalidInputMsg = '". _("This choice is not allowed")."';";
+	var invalidInputMsg = '". _("This choice is not allowed")."';
+	var groupId =".$this->Group->getID().";
+	var atId = ".$this->getID().";";
 		$javascript = <<<'EOS'
 	$.expr[':'].invalid = function(elem, index, match) {
 		for (let invalid of document.querySelectorAll(':invalid') )  {
@@ -1203,31 +1226,71 @@ class ArtifactTypeHtml extends ArtifactType {
 		}
 		return false;
 	};
-	$(".package").on('change', function(){
-		$(this).children('option:selected').each(function(i){
-			var releases = $(this).data("releases");
-			$("select[name^='extra_fields["+releases.field+"]']")[0].setCustomValidity("");
-			$("select[name^='extra_fields["+releases.field+"]'] option").not( "[value='100']" ).each(function(j,opt){
-				if (this.value!='100') {
-					if ($.inArray(parseInt(this.value),releases.elmnt)>-1) {
-						$(this).prop('disabled', false).removeClass('option_disabled');
-					} else {
-						$(this).prop('disabled', true);
-						$(this).addClass('option_disabled');
-					}
+	$("input[type='radio'].readonly, input[type='checkbox'].readonly").on('click', function(){
+		return false;
+	}).on('keydown', function(event){
+		if(event.keyCode !== 9) return false;
+	});
+	$(".in-formula[name^='extra_fields']").on('change', function(){
+		$.ajax({
+			type: 'POST',
+			url: 'index.php',
+			data: 'rtype=ajax&function=get_formulas_results&group_id='+groupId+'&atid='+atId+'&'+$("[name^='extra_fields']" ).serialize(),
+			async: false,
+			dataType: 'json',
+			success: function(answer){
+				if(answer['message']) {
+					showMessage(answer['message'], 'error');
 				}
-			});
-			$("select[name^='extra_fields["+releases.field+"]'] option:selected:disabled").parent().each(function() {
-				$(this).children('option:selected:disabled').prop('disabled', false);
-				this.setCustomValidity(invalidSelectMsg);
-				$(this).on("change.invalid", function() {
-					$(this).children('option.option_disabled:not(:disabled):not(:selected)').prop('disabled', true);
-					if (!$(this).children('option.option_disabled:selected').length) {
-						this.setCustomValidity("");
-						$(this).off("change.invalid");
+				fields = answer['fields'];
+				$.each(fields, function (index, field) {
+					fieldObj = $("[name^='extra_fields["+field.id+"]']");
+				console.log(fieldObj);
+				console.log(fieldObj.is("input[type='radio']"));
+					if (fieldObj.is("input[type='checkbox']")){
+				console.log("checkbox");
+						fieldObj.each(function() {
+							var in_array = -1;
+							for (var key in field.value) {
+								if (field.value[key] == $(this).val()) {
+									in_array = key;
+									break;
+								}
+							}
+							if (in_array > -1) {
+								$(this).prop("checked",true);
+							} else {
+								$(this).prop("checked",false);
+							}
+						});
+					} else if (fieldObj.is("input[type='radio']")){
+				console.log("radio");
+						fieldObj.each(function() {
+							console.log($(this));
+							var in_array = -1;
+							for (var key in field.value) {
+								if (field.value[key] == $(this).val()) {
+									in_array = key;
+									break;
+								}
+							}
+							console.log(in_array);
+							if (in_array > -1) {
+								$(this).prop("checked",true);
+							} else {
+								$(this).prop("checked",false);
+							}
+						});
+					} else if (fieldObj.is("input")){
+						fieldObj.val(field.value);
+					} else if (fieldObj.is("select")){
+						fieldObj.val(field.value);
+					}  else if (fieldObj.is("textarea")){
+						fieldObj.val(field.value);
 					}
 				});
-			});
+				return true;
+			}
 		});
 	});
 	$(".autoassign[name^='extra_fields']").on('change', function(){
diff --git a/src/common/tracker/views/form-updateextrafield.php b/src/common/tracker/views/form-updateextrafield.php
index 9e3ba7f..3d89ce2 100644
--- a/src/common/tracker/views/form-updateextrafield.php
+++ b/src/common/tracker/views/form-updateextrafield.php
@@ -190,6 +190,15 @@ if (!$ac || !is_object($ac)) {
 			break;
 	}
 
+	if (in_array($efType, array(ARTIFACT_EXTRAFIELDTYPE_TEXT, ARTIFACT_EXTRAFIELDTYPE_INTEGER, ARTIFACT_EXTRAFIELDTYPE_TEXTAREA))) {
+		echo html_ao('p');
+		echo html_e('label', array('for'=>'formula'), _('Formula to calculate field value'));
+		echo html_e('textarea', array('type'=>'text', 'name'=>'formula', 'rows'=>4, 'cols'=>50), $ac->getFormula(), false);
+		echo html_ac(html_ap() - 1);
+	} else {
+		echo html_e('input', array('type'=>'hidden', 'name'=>'formula', 'value'=>''));
+	}
+
 	echo $HTML->warning_msg(_('It is not recommended that you change the custom field name because other things are dependent upon it. When you change the custom field name, all related items will be changed to the new name.'));
 
 	echo html_ao('p');
diff --git a/src/common/tracker/views/form-updateextrafieldelement.php b/src/common/tracker/views/form-updateextrafieldelement.php
index cac96c3..e798fd6 100644
--- a/src/common/tracker/views/form-updateextrafieldelement.php
+++ b/src/common/tracker/views/form-updateextrafieldelement.php
@@ -90,6 +90,12 @@ if (!$ac || !is_object($ac)) {
 		} else {
 			echo html_e('input', array('type'=>'hidden', 'name'=>'auto_assign_to', 'value'=>100));
 		}
+
+		echo html_ao('p');
+		echo html_e('label', array('for'=>'formula'), html_e('strong', array(), _('Formula')._(':')).html_e('br'));
+		echo html_e('textarea', array('type'=>'text', 'id'=>'formula', 'name'=>'formula', 'rows'=>4, 'cols'=>50), $ao->getFormula(), false);
+		echo html_ac(html_ap()-1);
+
 		echo $HTML->warning_msg(_('It is not recommended that you change the custom field name because other things are dependent upon it. When you change the custom field name, all related items will be changed to the new name.'));
 		echo html_ao('p');
 		echo html_e('input', array('type'=>'submit', 'name'=>'post_changes', 'value'=> _('Update')));
diff --git a/src/www/include/expression.php b/src/www/include/expression.php
new file mode 100644
index 0000000..de27744
--- /dev/null
+++ b/src/www/include/expression.php
@@ -0,0 +1,597 @@
+<?php
+/**
+ *
+ * Expression - PHP Class to safely evaluate math expressions
+ * 
+ * Copyright 2005 Miles Kaufmann <http://www.twmagic.com/>
+ * Copyright 2012 - 2015 Johan Falk <http://magisterfalk.wordpress.com/>
+ * Copyright 2015 Colin Kiegel <http://colin-kiegel.github.io/>
+ * Copyright 2016 Jakub Jankiewicz <http://jcubic.pl/>
+ * Copyright 2016 Константин <https://github.com/optimistex>
+ * Copyright 2016 Stéphane-Eymeric Breddthauer - TrivaDev
+ *
+ * NAME
+ * Expression - safely evaluate math and boolean expressions
+ *
+ * SYNOPSIS
+ * <?
+ * include('expression.php');
+ * $e = new Expression();
+ * // basic evaluation:
+ * $result = $e->evaluate('2+2');
+ * // supports: order of operation; parentheses; negation; built-in functions
+ * $result = $e->evaluate('-8(5/2)^2*(1-sqrt(4))-8');
+ * // create your own variables
+ * $e->evaluate('a = e^(ln(pi))');
+ * // or functions
+ * $e->evaluate('f(x,y) = x^2 + y^2 - 2x*y + 1');
+ * // and then use them
+ * $result = $e->evaluate('3*f(42,a)');
+ * ?>
+ *
+ * DESCRIPTION
+ * Use the Expression class when you want to evaluate mathematical or boolean
+ * expressions from untrusted sources. You can define your own variables and
+ * functions, which are stored in the object. Try it, it's fun!
+ *
+ * Based on http://www.phpclasses.org/browse/file/11680.html, cred to Miles Kaufmann
+ *
+ * METHODS
+ * $e->evalute($expr)
+ * Evaluates the expression and returns the result. If an error occurs,
+ * prints a warning and returns false. If $expr is a function assignment,
+ * returns true on success.
+ *
+ * $e->e($expr)
+ * A synonym for $e->evaluate().
+ *
+ * $e->vars()
+ * Returns an associative array of all user-defined variables and values.
+ *
+ * $e->funcs()
+ * Returns an array of all user-defined functions.
+ *
+ * PARAMETERS
+ * $e->suppress_errors
+ * Set to true to turn off warnings when evaluating expressions
+ *
+ * $e->last_error
+ * If the last evaluation failed, contains a string describing the error.
+ * (Useful when suppress_errors is on).
+ *
+ * LICENSE (BSD 3 Clause)
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+class Expression {
+	var $suppress_errors = false;
+	var $last_error = null;
+	var $v = array ('e' => 2.71, 'pi' => 3.14); // variables (and constants)
+	var $f = array (); // user-defined functions
+	var $vb = array ('e', 'pi'); // constants
+	var $fb = array ( // built-in functions
+			'sin', 'sinh', 'arcsin', 'asin', 'arcsinh', 'asinh',
+			'cos', 'cosh', 'arccos', 'acos', 'arccosh', 'acosh',
+			'tan', 'tanh', 'arctan', 'atan', 'arctanh', 'atanh',
+			'sqrt', 'abs', 'ln', 'log');
+	var $functions = array (); // function defined outside of Expression as closures
+	function __construct() {
+		// make the variables a little more accurate
+		$this->v ['pi'] = pi ();
+		$this->v ['e'] = exp ( 1 );
+	}
+	function e($expr) {
+		return $this->evaluate ( $expr );
+	}
+	function evaluate($expr) {
+		$this->last_error = null;
+		$expr = trim ( $expr );
+		if (substr ( $expr, - 1, 1 ) == ';') {
+			$expr = substr ( $expr, 0, strlen ( $expr ) - 1 ); // strip semicolons at the end
+		}
+		// ===============
+		// is it a variable assignment?
+		if (preg_match ( '/^\s*([a-z]\w*)\s*=(?!~|=)\s*(.+)$/', $expr, $matches )) {
+			if (in_array ( $matches [1], $this->vb )) { // make sure we're not assigning to a constant
+				return $this->trigger ( "cannot assign to constant '$matches[1]'" );
+			}
+			$tmp = $this->pfx ( $this->nfx ( $matches [2] ) );
+			$this->v [$matches [1]] = $tmp; // if so, stick it in the variable array
+			return $this->v [$matches [1]]; // and return the resulting value
+		// ===============
+		// is it a function assignment?
+		} elseif (preg_match ( '/^\s*([a-z]\w*)\s*\((?:\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*)?\)\s*=(?!~|=)\s*(.+)$/', $expr, $matches )) {
+			$fnn = $matches [1]; // get the function name
+			if (in_array ( $matches [1], $this->fb )) { // make sure it isn't built in
+				return $this->trigger ( "cannot redefine built-in function '$matches[1]()'" );
+			}
+			if ($matches [2] != "") {
+				$args = explode ( ",", preg_replace ( "/\s+/", "", $matches [2] ) ); // get the arguments
+			} else {
+				$args = array ();
+			}
+			if (($stack = $this->nfx ( $matches [3] )) === false) {
+				return false; // see if it can be converted to postfix
+			}
+			for($i = 0; $i < count ( $stack ); $i ++) { // freeze the state of the non-argument variables
+				$token = $stack [$i];
+				if (preg_match ( '/^[a-z]\w*$/', $token ) and ! in_array ( $token, $args )) {
+					if (array_key_exists ( $token, $this->v )) {
+						$stack [$i] = $this->v [$token];
+					} else {
+						return $this->trigger ( "undefined variable '$token' in function definition" );
+					}
+				}
+			}
+			$this->f [$fnn] = array (
+					'args' => $args,
+					'func' => $stack 
+			);
+			return true;
+			// ===============
+		} else {
+			return $this->pfx ( $this->nfx ( $expr ) ); // straight up evaluation, woo
+		}
+	}
+	function vars() {
+		$output = $this->v;
+		unset ( $output ['pi'] );
+		unset ( $output ['e'] );
+		return $output;
+	}
+	function funcs() {
+		$output = array ();
+		foreach ( $this->f as $fnn => $dat ) {
+			$output [] = $fnn . '(' . implode ( ',', $dat ['args'] ) . ')';
+		}
+		return $output;
+	}
+	
+	// ===================== HERE BE INTERNAL METHODS ====================\\
+	
+	// Convert infix to postfix notation
+	function nfx($expr) {
+		$index = 0;
+		$stack = new ExpressionStack ();
+		$output = array (); // postfix form of expression, to be passed to pfx()
+		$expr = trim ( strtolower ( $expr ) );
+		
+		$ops = array ('+', '-', '*', '/', '^', '_', '%', '>', '<', '>=', '<=', '==', '!=', '=~', '&&', '||', '!', '?', ':', '?:');
+		$ops_r = array ('+' => 0, '-' => 0, '*' => 0, '/' => 0, '^' => 1, '_' => 0, '%' => 0, '>' => 0, '<' => 0, '>=' => 0, '<=' => 0, '==' => 0, '!=' => 0, '=~' => 0, '&&' => 0, '||' => 0, '!' => 0, '?' => 1, ':' => 0, '?:' => 0); // right-associative operator?
+		// $ops_p = array('+'=>4,'-'=>4,'*'=>4,'/'=>4,'_'=>4,'%'=>4,'^'=>5,'>'=>2,'<'=>2,
+		// '>='=>2,'<='=>2,'=='=>2,'!='=>2,'=~'=>2,'&&'=>1,'||'=>1,'!'=>5); // operator precedence
+		$ops_p = array (
+				':' => 0, ':?' => 0,
+				'?' => 1,
+				'&&' => 2, '||' => 2, 
+				'>' => 3, '<' => 3, '>=' => 3, '<=' => 3, '==' => 3, '!=' => 3, '=~' => 3,
+				'+' => 4, '-' => 4,
+				'*' => 5, '/' => 5, '_' => 5, '%' => 5,
+				'^' => 6, '!' => 6); // operator precedence
+		$expecting_op = false; // we use this in syntax-checking the expression
+								// and determining when a - is a negation
+		
+		/*
+		 * we allow all characters because of strings
+		 * if (preg_match("%[^\w\s+*^\/()\.,-<>=&~|!\"\\\\/]%", $expr, $matches)) { // make sure the characters are all good
+		 * return $this->trigger("illegal character '{$matches[0]}'");
+		 * }
+		 */
+		$first_argument = false;
+		$ternary = false;
+		while ( 1 ) { // 1 Infinite Loop ;)
+			$op = substr ( $expr, $index, 2 ); // get the first two characters at the current index
+			if (preg_match ( "/^[+\-*\/^_\"<>=%(){\[!~,?:](?!=|~)/", $op ) || preg_match ( "/\w/", $op )) {
+				// fix $op if it should have one character
+				$op = substr ( $expr, $index, 1 );
+			}
+			$single_str = '(?<!\\\\)"(?:(?:(?<!\\\\)(?:\\\\{2})*\\\\)"|[^"])*(?<![^\\\\]\\\\)"';
+			$double_str = "(?<!\\\\)'(?:(?:(?<!\\\\)(?:\\\\{2})*\\\\)'|[^'])*(?<![^\\\\]\\\\)'";
+			$json = '[\[{](?>"(?:[^"]|\\\\")*"|[^[{\]}]|(?1))*[\]}]';
+			$number = '[\d.]+e\d+|\d+(?:\.\d*)?|\.\d+';
+			$name = '[a-z]\w*\(?|\\$\w+';
+			$parenthesis = '\\(';
+			// find out if we're currently at the beginning of a number/string/object/array/variable/function/parenthesis/operand
+			$ex = preg_match ( "%^($single_str|$double_str|$json|$name|$number|$parenthesis)%", substr ( $expr, $index ), $match );
+			// ===============
+			if ($op == '[' && $expecting_op && $ex) {
+				if (! preg_match ( "/^\[(.*)\]$/", $match [1], $matches )) {
+					return $this->trigger ( "invalid array access" );
+				}
+				$stack->push ( '[' );
+				$stack->push ( $matches [1] );
+				$index += strlen ( $match [1] );
+				// } elseif ($op == '!' && !$expecting_op) {
+				// $stack->push('!'); // put a negation on the stack
+				// $index++;
+			} elseif ($op == '-' and ! $expecting_op) { // is it a negation instead of a minus?
+				$stack->push ( '_' ); // put a negation on the stack
+				$index ++;
+			} elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack
+				return $this->trigger ( "illegal character '_'" ); // but not in the input expression
+			} elseif (((in_array ( $op, $ops ) or $ex) and $expecting_op) or in_array ( $op, $ops ) and ! $expecting_op) {
+				// are we putting an operator on the stack?
+				if ($ex) { // are we expecting an operator but have a number/variable/function/opening parethesis?
+					$op = '*';
+					$index --; // it's an implicit multiplication
+				}
+				// heart of the algorithm:
+				$o2 = $stack->last ();
+				while ( $stack->count > 0 and ($o2 = $stack->last ()) and in_array ( $o2, $ops ) and ($ops_r [$op] ? $ops_p [$op] < $ops_p [$o2] : $ops_p [$op] <= $ops_p [$o2]) ) {
+					$val = $stack->pop ();
+					if ($val != '?') {
+						$output [] = $val; // pop stuff off the stack into the output
+					} else {
+						$op = '?:';
+						break;	
+					}
+				}
+				// many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
+				$stack->push ( $op ); // finally put OUR operator onto the stack
+				$index ++;
+				if (strlen ( $op ) == 2 && $op != '?:') {
+					$index ++;
+				}
+				$expecting_op = false;
+				// ===============
+			} elseif ($op == ')' and $expecting_op || ! $ex) { // ready to close a parenthesis?
+				while ( ($o2 = $stack->pop ()) != '(' ) { // pop off the stack back to the last (
+					if (is_null ( $o2 )) {
+						return $this->trigger ( "unexpected ')'" );
+					} else {
+						$output [] = $o2;
+					}
+				}
+				if (preg_match ( "/^([a-z]\w*)\($/", $stack->last ( 2 ), $matches )) { // did we just close a function?
+					$fnn = $matches [1]; // get the function name
+					$arg_count = $stack->pop (); // see how many arguments there were (cleverly stored on the stack, thank you)
+					$val = $stack->pop();
+					$output [] = $val; // pop the function and push onto the output
+					if (in_array ( $fnn, $this->fb )) { // check the argument count
+						if ($arg_count > 1) {
+							return $this->trigger ( "too many arguments ($arg_count given, 1 expected)" );
+						}
+					} elseif (array_key_exists ( $fnn, $this->f )) {
+						if ($arg_count != count ( $this->f [$fnn] ['args'] )) {
+							return $this->trigger ( "wrong number of arguments ($arg_count given, " . count ( $this->f [$fnn] ['args'] ) . " expected) " . json_encode ( $this->f [$fnn] ['args'] ) );
+						}
+					} elseif (array_key_exists ( $fnn, $this->functions )) {
+						$func_reflection = new ReflectionFunction ( $this->functions [$fnn] );
+						$count = $func_reflection->getNumberOfParameters ();
+						if ($arg_count != $count) {
+							return $this->trigger ( "wrong number of arguments ($arg_count given, " . $count . " expected)" );
+						}
+					} else { // did we somehow push a non-function on the stack? this should never happen
+						return $this->trigger ( "internal error" );
+					}
+				}
+				$index ++;
+				// ===============
+			} elseif ($op == ',' and $expecting_op) { // did we just finish a function argument?
+				while ( ($o2 = $stack->pop ()) != '(' ) {
+					if (is_null ( $o2 )) {
+						return $this->trigger ( "unexpected ','" ); // oops, never had a (
+					} else {
+						$output [] = $o2; // pop the argument expression stuff and push onto the output
+					}
+				}
+				// make sure there was a function
+				if (! preg_match ( "/^([a-z]\w*)\($/", $stack->last ( 2 ), $matches )) {
+					return $this->trigger ( "unexpected ','" );
+				}
+				if ($first_argument) {
+					$first_argument = false;
+				} else {
+					$val = $stack->pop ();
+					$stack->push ( $val + 1 ); // increment the argument count
+				}
+				$stack->push ( '(' ); // put the ( back on, we'll need to pop back to it again
+				$index ++;
+				$expecting_op = false;
+				// ===============
+			} elseif ($op == '(' and ! $expecting_op) {
+				$stack->push ( '(' ); // that was easy
+				$index ++;
+				$allow_neg = true;
+				// ===============
+			} elseif ($ex and ! $expecting_op) { // do we now have a function/variable/number?
+				$expecting_op = true;
+				$val = $match [1];
+				if ($op == '[' || $op == "{" || preg_match ( "/null|true|false/", $match [1] )) {
+					$output [] = $val;
+				} elseif (preg_match ( "/^([a-z]\w*)\($/", $val, $matches )) { // may be func, or variable w/ implicit multiplication against parentheses...
+					if (in_array ( $matches [1], $this->fb ) or array_key_exists ( $matches [1], $this->f ) or array_key_exists ( $matches [1], $this->functions )) { // it's a func
+						$stack->push ( $val );
+						$stack->push ( 0 );
+						$stack->push ( '(' );
+						$expecting_op = false;
+					} else { // it's a var w/ implicit multiplication
+						$val = $matches [1];
+						$output [] = $val;
+					}
+				} else { // it's a plain old var or num
+					$output [] = $val;
+					if (preg_match ( "/^([a-z]\w*)\($/", $stack->last ( 3 ) )) {
+						$first_argument = true;
+						while ( ($o2 = $stack->pop ()) != '(' ) {
+							if (is_null ( $o2 )) {
+								return $this->trigger ( "unexpected error" ); // oops, never had a (
+							} else {
+								$output [] = $o2; // pop the argument expression stuff and push onto the output
+							}
+						}
+						// make sure there was a function
+						if (! preg_match ( "/^([a-z]\w*)\($/", $stack->last ( 2 ), $matches )) {
+							return $this->trigger ( "unexpected error" );
+						}
+						$val = $stack->pop ();
+						$stack->push ( $val + 1 ); // increment the argument count
+						$stack->push ( '(' ); // put the ( back on, we'll need to pop back to it again
+					}
+				}
+				$index += strlen ( $val );
+				// ===============
+			} elseif ($op == ')') { // miscellaneous error checking
+				return $this->trigger ( "unexpected ')'" );
+			} elseif (in_array ( $op, $ops ) and ! $expecting_op) {
+				return $this->trigger ( "unexpected operator '$op'" );
+			} else { // I don't even want to know what you did to get here
+				return $this->trigger ( "an unexpected error occured " . json_encode ( $op ) . " " . json_encode ( $match ) . " " . ($ex ? 'true' : 'false') . " " . $expr );
+			}
+			if ($index == strlen ( $expr )) {
+				if (in_array ( $op, $ops )) { // did we end with an operator? bad.
+					return $this->trigger ( "operator '$op' lacks operand" );
+				} else {
+					break;
+				}
+			}
+			while ( substr ( $expr, $index, 1 ) == ' ' ) { // step the index past whitespace (pretty much turns whitespace
+				$index ++; // into implicit multiplication if no operator is there)
+			}
+		}
+		while ( ! is_null ( $op = $stack->pop () ) ) { // pop everything off the stack and push onto output
+			if ($op == '(') {
+				return $this->trigger ( "expecting ')'" ); // if there are (s on the stack, ()s were unbalanced
+			}
+			$output [] = $op;
+		}
+		return $output;
+	}
+	
+	// evaluate postfix notation
+	function pfx($tokens, $vars = array()) {
+		$binaryOperator = array ('+', '-', '*', '/', '^', '<', '>', '<=', '>=', '==', '&&', '||', '!=', '=~', '%');
+		if ($tokens == false) {
+			return false;
+		}
+		$stack = new ExpressionStack ();
+		foreach ( $tokens as $token ) { // nice and easy
+			if ($token == '?:') {
+				$op1 = $stack->pop ();
+				$op2 = $stack->pop ();
+				$op3 = $stack->pop ();
+				if ($op3) {
+					$stack->push ( $op2 );
+				} else {
+					$stack->push ( $op1 );
+				}
+				// if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
+			} elseif (in_array ( $token, $binaryOperator)) {
+				$isTernary = false;
+				$op2 = $stack->pop ();
+				$op1 = $stack->pop ();
+				switch ($token) {
+					case '+' :
+						if (is_string ( $op1 ) || is_string ( $op2 )) {
+							$stack->push ( ( string ) $op1 . ( string ) $op2 );
+						} else {
+							$stack->push ( $op1 + $op2 );
+						}
+						break;
+					case '-' :
+						$stack->push ( $op1 - $op2 );
+						break;
+					case '*' :
+						$stack->push ( $op1 * $op2 );
+						break;
+					case '/' :
+						if ($op2 == 0)
+							return $this->trigger ( "division by zero" );
+						$stack->push ( $op1 / $op2 );
+						break;
+					case '%' :
+						$stack->push ( $op1 % $op2 );
+						break;
+					case '^' :
+						$stack->push ( pow ( $op1, $op2 ) );
+						break;
+					case '>' :
+						$stack->push ( $op1 > $op2 );
+						break;
+					case '<' :
+						$stack->push ( $op1 < $op2 );
+						break;
+					case '>=' :
+						$stack->push ( $op1 >= $op2 );
+						break;
+					case '<=' :
+						$stack->push ( $op1 <= $op2 );
+						break;
+					case '==' :
+						if (is_array ( $op1 ) && is_array ( $op2 )) {
+							$stack->push ( json_encode ( $op1 ) == json_encode ( $op2 ) );
+						} else {
+							$stack->push ( $op1 == $op2 );
+						}
+						break;
+					case '!=' :
+						if (is_array ( $op1 ) && is_array ( $op2 )) {
+							$stack->push ( json_encode ( $op1 ) != json_encode ( $op2 ) );
+						} else {
+							$stack->push ( $op1 != $op2 );
+						}
+						break;
+					case '=~' :
+						$value = @preg_match ( $op2, $op1, $match );
+						
+						if (! is_int ( $value )) {
+							return $this->trigger ( "Invalid regex " . json_encode ( $op2 ) );
+						}
+						$stack->push ( $value );
+						for($i = 0; $i < count ( $match ); $i ++) {
+							$this->v ['$' . $i] = $match [$i];
+						}
+						break;
+					case '&&' :
+						$stack->push ( $op1 ? $op2 : $op1 );
+						break;
+					case '||' :
+						$stack->push ( $op1 ? $op1 : $op2 );
+						break;
+				}
+				// if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
+			} elseif ($token == '!') {
+				$isTernary = false;
+				$stack->push ( ! $stack->pop () );
+			} elseif ($token == '[') {
+				$isTernary = false;
+				$selector = $stack->pop ();
+				$object = $stack->pop ();
+				if (is_object ( $object )) {
+					$stack->push ( $object->$selector );
+				} elseif (is_array ( $object )) {
+					$stack->push ( $object [$selector] );
+				} else {
+					return $this->trigger ( "invalid object for selector" );
+				}
+			} elseif ($token == "_") {
+				$stack->push ( - 1 * $stack->pop () );
+				// if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
+			} elseif (preg_match ( "/^([a-z]\w*)\($/", $token, $matches )) { // it's a function!
+				$fnn = $matches [1];
+				if (in_array ( $fnn, $this->fb )) { // built-in function:
+					if (is_null ( $op1 = $stack->pop () ))
+						return $this->trigger ( "internal error" );
+					$fnn = preg_replace ( "/^arc/", "a", $fnn ); // for the 'arc' trig synonyms
+					if ($fnn == 'ln')
+						$fnn = 'log';
+					$stack->push ( $fnn ( $op1 ) ); // perfectly safe variable function call
+				} elseif (array_key_exists ( $fnn, $this->f )) { // user function
+					// get args
+					$args = array ();
+					for($i = count ( $this->f [$fnn] ['args'] ) - 1; $i >= 0; $i --) {
+						if ($stack->isEmpty ()) {
+							return $this->trigger ( "internal error " . $fnn . " " . json_encode ( $this->f [$fnn] ['args'] ) );
+						}
+						$args [$this->f [$fnn] ['args'] [$i]] = $stack->pop ();
+					}
+					$stack->push ( $this->pfx ( $this->f [$fnn] ['func'], $args ) ); // yay... recursion!!!!
+				} elseif (array_key_exists ( $fnn, $this->functions )) {
+					$reflection = new ReflectionFunction ( $this->functions [$fnn] );
+					$count = $reflection->getNumberOfParameters ();
+					for($i = $count - 1; $i >= 0; $i --) {
+						if ($stack->isEmpty ()) {
+							return $this->trigger ( "internal error" );
+						}
+						$args [] = $stack->pop ();
+					}
+					$stack->push ( $reflection->invokeArgs ( $args ) );
+				}
+				// if the token is a number or variable, push it on the stack
+			} else {
+				if (preg_match ( '/^([\[{](?>"(?:[^"]|\\")*"|[^[{\]}]|(?1))*[\]}])$/', $token ) || preg_match ( "/^(null|true|false)$/", $token )) { // json
+				                                               // return $this->trigger("invalid json " . $token);
+					if ($token == 'null') {
+						$value = null;
+					} elseif ($token == 'true') {
+						$value = true;
+					} elseif ($token == 'false') {
+						$value = false;
+					} else {
+						$value = json_decode ( $token );
+						if ($value == null) {
+							return $this->trigger ( "invalid json " . $token );
+						}
+					}
+					$stack->push ( $value );
+				} elseif (is_numeric ( $token )) {
+					$stack->push ( 0 + $token );
+				} else if (preg_match ( "/^['\\\"](.*)['\\\"]$/", $token )) {
+					$stack->push ( json_decode ( preg_replace_callback ( "/^['\\\"](.*)['\\\"]$/", function ($matches) {
+						$m = array ("/\\\\'/", '/(?<!\\\\)"/');
+						$r = array ("'", '\\"' );
+						return '"' . preg_replace ( $m, $r, $matches [1] ) . '"';
+					}, $token ) ) );
+				} elseif (array_key_exists ( $token, $this->v )) {
+					$stack->push ( $this->v [$token] );
+				} elseif (array_key_exists ( $token, $vars )) {
+					$stack->push ( $vars [$token] );
+				} else {
+					return $this->trigger ( "undefined variable '$token'" );
+				}
+			}
+		}
+		// when we're out of tokens, the stack should have a single element, the final result
+		if ($stack->count != 1)
+			return $this->trigger ( "internal error" );
+		return $stack->pop ();
+	}
+	
+	// trigger an error, but nicely, if need be
+	function trigger($msg) {
+		$this->last_error = $msg;
+		if (! $this->suppress_errors)
+			trigger_error ( $msg, E_USER_WARNING );
+		return false;
+	}
+}
+
+// for internal use
+class ExpressionStack {
+	var $stack = array ();
+	var $count = 0;
+	function push($val) {
+		$this->stack [$this->count] = $val;
+		$this->count ++;
+	}
+	function pop() {
+		if ($this->count > 0) {
+			$this->count --;
+			return $this->stack [$this->count];
+		}
+		return null;
+	}
+	function isEmpty() {
+		return empty ( $this->stack );
+	}
+	function last($n = 1) {
+		if (isset ( $this->stack [$this->count - $n] )) {
+			return $this->stack [$this->count - $n];
+		}
+		return;
+	}
+}
diff --git a/src/www/themes/css/fusionforge.css b/src/www/themes/css/fusionforge.css
index 02e1169..a8d55be 100644
--- a/src/www/themes/css/fusionforge.css
+++ b/src/www/themes/css/fusionforge.css
@@ -1086,3 +1086,7 @@ table.id_sortable th.headerSortDown span {
 .xdsoft_datetimepicker .active {
 	background: none;
 }
+
+input[readonly], select[readonly] {
+	background-color:#ebebeb;
+}

-----------------------------------------------------------------------

Summary of changes:
 src/common/tracker/ArtifactExtraField.class.php    |  74 +++
 .../tracker/ArtifactExtraFieldElement.class.php    |  55 ++
 src/common/tracker/ArtifactType.class.php          |  45 ++
 src/common/tracker/actions/admin-updates.php       |  24 +-
 src/common/tracker/actions/ajax.php                |  81 +++
 .../tracker/include/ArtifactTypeHtml.class.php     | 111 +++-
 src/common/tracker/views/form-updateextrafield.php |   9 +
 .../tracker/views/form-updateextrafieldelement.php |   6 +
 src/db/20161128-tracker-field-formula.sql          |  10 +
 src/www/include/expression.php                     | 597 +++++++++++++++++++++
 src/www/themes/css/fusionforge.css                 |   4 +
 11 files changed, 986 insertions(+), 30 deletions(-)
 create mode 100644 src/db/20161128-tracker-field-formula.sql
 create mode 100644 src/www/include/expression.php


hooks/post-receive
-- 
FusionForge



More information about the Fusionforge-commits mailing list