Преглед изворни кода

Custom fields: alpha version.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3943 a333f486-631f-4898-b8df-5754b55c2be0
romainq пре 9 година
родитељ
комит
0489e8e5e8

+ 70 - 13
application/cmdbabstract.class.inc.php

@@ -44,6 +44,7 @@ require_once(APPROOT.'/application/ui.passwordwidget.class.inc.php');
 require_once(APPROOT.'/application/ui.extkeywidget.class.inc.php');
 require_once(APPROOT.'/application/ui.htmleditorwidget.class.inc.php');
 require_once(APPROOT.'/application/datatable.class.inc.php');
+require_once(APPROOT.'/sources/renderer/console/consoleformrenderer.class.inc.php');
 
 abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 {
@@ -1712,7 +1713,8 @@ EOF
 			{
 				$bMandatory = 'true';
 			}
-			$sValidationField = "<span class=\"form_validation\" id=\"v_{$iId}\"></span>";
+			$sValidationSpan = "<span class=\"form_validation\" id=\"v_{$iId}\"></span>";
+			$sReloadSpan = "<span class=\"field_status\" id=\"fstatus_{$iId}\"></span>";
 			$sHelpText = htmlentities($oAttDef->GetHelpOnEdition(), ENT_QUOTES, 'UTF-8');
 			$aEventsList = array();
 			switch($oAttDef->GetEditClass())
@@ -1725,7 +1727,7 @@ EOF
 				{
 					$sDisplayValue = date($oAttDef->GetDateFormat());
 				}
-				$sHTMLValue = "<input title=\"$sHelpText\" class=\"date-pick\" type=\"text\" size=\"12\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" value=\"".htmlentities($sDisplayValue, ENT_QUOTES, 'UTF-8')."\" id=\"$iId\"/>&nbsp;{$sValidationField}";
+				$sHTMLValue = "<input title=\"$sHelpText\" class=\"date-pick\" type=\"text\" size=\"12\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" value=\"".htmlentities($sDisplayValue, ENT_QUOTES, 'UTF-8')."\" id=\"$iId\"/>&nbsp;{$sValidationSpan}{$sReloadSpan}";
 				break;
 
 				case 'DateTime':
@@ -1736,7 +1738,7 @@ EOF
 				{
 					$sDisplayValue = date($oAttDef->GetDateFormat());
 				}
-				$sHTMLValue = "<input title=\"$sHelpText\" class=\"datetime-pick\" type=\"text\" size=\"20\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" value=\"".htmlentities($sDisplayValue, ENT_QUOTES, 'UTF-8')."\" id=\"$iId\"/>&nbsp;{$sValidationField}";
+				$sHTMLValue = "<input title=\"$sHelpText\" class=\"datetime-pick\" type=\"text\" size=\"20\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" value=\"".htmlentities($sDisplayValue, ENT_QUOTES, 'UTF-8')."\" id=\"$iId\"/>&nbsp;{$sValidationSpan}{$sReloadSpan}";
 				break;
 
 				case 'Duration':
@@ -1752,7 +1754,7 @@ EOF
 				$sMinutes = "<input title=\"$sHelpText\" type=\"text\" style=\"text-align:right\" size=\"2\" name=\"attr_{$sFieldPrefix}{$sAttCode}[m]{$sNameSuffix}\" value=\"{$aVal['minutes']}\" id=\"{$iId}_m\"/>";
 				$sSeconds = "<input title=\"$sHelpText\" type=\"text\" style=\"text-align:right\" size=\"2\" name=\"attr_{$sFieldPrefix}{$sAttCode}[s]{$sNameSuffix}\" value=\"{$aVal['seconds']}\" id=\"{$iId}_s\"/>";
 				$sHidden = "<input type=\"hidden\" id=\"{$iId}\" value=\"".htmlentities($value, ENT_QUOTES, 'UTF-8')."\"/>";
-				$sHTMLValue = Dict::Format('UI:DurationForm_Days_Hours_Minutes_Seconds', $sDays, $sHours, $sMinutes, $sSeconds).$sHidden."&nbsp;".$sValidationField;
+				$sHTMLValue = Dict::Format('UI:DurationForm_Days_Hours_Minutes_Seconds', $sDays, $sHours, $sMinutes, $sSeconds).$sHidden."&nbsp;".$sValidationSpan.$sReloadSpan;
 				$oPage->add_ready_script("$('#{$iId}').bind('update', function(evt, sFormId) { return ToggleDurationField('$iId'); });");				
 				break;
 				
@@ -1760,7 +1762,7 @@ EOF
 					$aEventsList[] ='validate';
 					$aEventsList[] ='keyup';
 					$aEventsList[] ='change';
-					$sHTMLValue = "<input title=\"$sHelpText\" type=\"password\" size=\"30\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" value=\"".htmlentities($value, ENT_QUOTES, 'UTF-8')."\" id=\"$iId\"/>&nbsp;{$sValidationField}";
+					$sHTMLValue = "<input title=\"$sHelpText\" type=\"password\" size=\"30\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" value=\"".htmlentities($value, ENT_QUOTES, 'UTF-8')."\" id=\"$iId\"/>&nbsp;{$sValidationSpan}{$sReloadSpan}";
 				break;
 				
 				case 'OQLExpression':
@@ -1799,7 +1801,7 @@ EOF
 						$sAdditionalStuff = "";
 					}
 					// Ok, the text area is drawn here
-					$sHTMLValue = "<table><tr><td><textarea class=\"resizable\" title=\"$sHelpText\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" rows=\"8\" cols=\"40\" id=\"$iId\" $sStyle>".htmlentities($sEditValue, ENT_QUOTES, 'UTF-8')."</textarea>$sAdditionalStuff</td><td>{$sValidationField}</td></tr></table>";
+					$sHTMLValue = "<table><tr><td><textarea class=\"resizable\" title=\"$sHelpText\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" rows=\"8\" cols=\"40\" id=\"$iId\" $sStyle>".htmlentities($sEditValue, ENT_QUOTES, 'UTF-8')."</textarea>$sAdditionalStuff</td><td>{$sValidationSpan}{$sReloadSpan}</td></tr></table>";
 
 				break;
 
@@ -1825,12 +1827,12 @@ EOF
 					$sPreviousLog = is_object($value) ? $value->GetAsHTML($oPage, true /* bEditMode */, array('AttributeText', 'RenderWikiHtml')) : '';
 					$iEntriesCount = is_object($value) ? count($value->GetIndex()) : 0;
 					$sHidden = "<input type=\"hidden\" id=\"{$iId}_count\" value=\"$iEntriesCount\"/>"; // To know how many entries the case log already contains
-					$sHTMLValue = "<div class=\"caselog\" $sStyle><table style=\"width:100%;\"><tr><td>$sHeader<textarea class=\"htmlEditor\" style=\"border:0;width:100%\" title=\"$sHelpText\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" rows=\"8\" cols=\"40\" id=\"$iId\">".htmlentities($sEditValue, ENT_QUOTES, 'UTF-8')."</textarea>$sPreviousLog</td><td>{$sValidationField}</td></tr></table>$sHidden</div>";
+					$sHTMLValue = "<div class=\"caselog\" $sStyle><table style=\"width:100%;\"><tr><td>$sHeader<textarea class=\"htmlEditor\" style=\"border:0;width:100%\" title=\"$sHelpText\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" rows=\"8\" cols=\"40\" id=\"$iId\">".htmlentities($sEditValue, ENT_QUOTES, 'UTF-8')."</textarea>$sPreviousLog</td><td>{$sValidationSpan}{$sReloadSpan}</td></tr></table>$sHidden</div>";
 					$oPage->add_ready_script("$('#$iId').bind('keyup change validate', function(evt, sFormId) { return ValidateCaseLogField('$iId', $bMandatory, sFormId) } );"); // Custom validation function
 				break;
 
 				case 'HTML':
-					$oWidget = new UIHTMLEditorWidget($iId, $oAttDef, $sNameSuffix, $sFieldPrefix, $sHelpText, $sValidationField, $value, $bMandatory);
+					$oWidget = new UIHTMLEditorWidget($iId, $oAttDef, $sNameSuffix, $sFieldPrefix, $sHelpText, $sValidationSpan.$sReloadSpan, $value, $bMandatory);
 					$sHTMLValue = $oWidget->Display($oPage, $aArgs);
 				break;
 
@@ -1862,7 +1864,7 @@ EOF
 					$sHTMLValue = "<input type=\"hidden\" name=\"MAX_FILE_SIZE\" value=\"$iMaxFileSize\" />\n";
 					$sHTMLValue .= "<input name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}[filename]\" type=\"hidden\" id=\"$iId\" \" value=\"".htmlentities($sFileName, ENT_QUOTES, 'UTF-8')."\"/>\n";
 					$sHTMLValue .= "<span id=\"name_$iInputId\">$sFileName</span><br/>\n";
-					$sHTMLValue .= "<input title=\"$sHelpText\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}[fcontents]\" type=\"file\" id=\"file_$iId\" onChange=\"UpdateFileName('$iId', this.value)\"/>&nbsp;{$sValidationField}\n";
+					$sHTMLValue .= "<input title=\"$sHelpText\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}[fcontents]\" type=\"file\" id=\"file_$iId\" onChange=\"UpdateFileName('$iId', this.value)\"/>&nbsp;{$sValidationSpan}{$sReloadSpan}\n";
 				break;
 				
 				case 'StopWatch':
@@ -1902,12 +1904,59 @@ EOF
 					$sHTMLValue .= $oAttDef->GetDisplayForm($value, $oPage, true);
 					$sHTMLValue .= '</div>';
 					$sHTMLValue .= '</td>';
-					$sHTMLValue .= '<td>'.$sValidationField.'</td>';
+					$sHTMLValue .= '<td>'.$sValidationSpan.$sReloadSpan.'</td>';
 					$sHTMLValue .= '</tr>';
 					$sHTMLValue .= '</table>';
 					$oPage->add_ready_script("$('#$iId :input').bind('keyup change validate', function(evt, sFormId) { return ValidateRedundancySettings('$iId',sFormId); } );"); // Custom validation function
 					break;
 
+				case 'CustomFields':
+					$sHTMLValue = '<table>';
+					$sHTMLValue .= '<tr>';
+					$sHTMLValue .= '<td>';
+					$sHTMLValue .= '<div id="'.$iId.'_console_form">';
+					$sHTMLValue .= '<div id="'.$iId.'_field_set">';
+					$sHTMLValue .= '</div>';
+					$sHTMLValue .= '</div>';
+					$sHTMLValue .= '</td>';
+					$sHTMLValue .= '<td>'.$sReloadSpan.'</td>'; // No validation span for this one: it does handle its own validation!
+					$sHTMLValue .= '</tr>';
+					$sHTMLValue .= '</table>';
+					$sHTMLValue .= "<input name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" type=\"hidden\" id=\"$iId\" value=\"\"/>\n";
+
+					$oForm = $value->GetForm($sFormPrefix);
+					$oRenderer = new \Combodo\iTop\Renderer\Console\ConsoleFormRenderer($oForm);
+					$aRenderRes = $oRenderer->Render();
+
+					$aFormHandlerOptions = array(
+						'wizard_helper_var_name' => 'oWizardHelper'.$sFormPrefix,
+						'custom_field_attcode' => $sAttCode
+					);
+					$sFormHandlerOptions = json_encode($aFormHandlerOptions);
+					$aFieldSetOptions = array(
+						'field_identifier_attr' => 'data-field-id', // convention: fields are rendered into a div and are identified by this attribute
+						'fields_list' => $aRenderRes,
+						'fields_impacts' => $oForm->GetFieldsImpacts(),
+						'form_path' => $oForm->GetId()
+					);
+					$sFieldSetOptions = json_encode($aFieldSetOptions);
+					$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/form_handler.js');
+					$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/console_form_handler.js');
+					$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/field_set.js');
+					$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/form_field.js');
+					$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/subform_field.js');
+					$oPage->add_ready_script("$('#{$iId}_console_form').console_form_handler($sFormHandlerOptions);");
+					$oPage->add_ready_script("$('#{$iId}_field_set').field_set($sFieldSetOptions);");
+					$oPage->add_ready_script("$('#{$iId}_console_form').console_form_handler('alignColumns');");
+					$oPage->add_ready_script("$('#{$iId}_console_form').console_form_handler('option', 'field_set', $('#{$iId}_field_set'));");
+					// field_change must be processed to refresh the hidden value at anytime
+					$oPage->add_ready_script("$('#{$iId}_console_form .field_set').bind('field_change', function() { $('#{$iId}').val(JSON.stringify($('#{$iId}_field_set').triggerHandler('get_current_values'))); });");
+					// update_value is triggered when preparing the wizard helper object for ajax calls
+					$oPage->add_ready_script("$('#{$iId}').bind('update_value', function() { $(this).val(JSON.stringify($('#{$iId}_field_set').triggerHandler('get_current_values'))); });");
+					// validate is triggered by CheckFields, on all the input fields, once at page init and once before submitting the form
+					$oPage->add_ready_script("$('#{$iId}').bind('validate', function(evt, sFormId) { return ValidateCustomFields('$iId', sFormId) } );"); // Custom validation function
+					break;
+
 				case 'String':
 				default:
 					$aEventsList[] ='validate';
@@ -1925,7 +1974,7 @@ EOF
 							case 'radio_vertical':
 							$sHTMLValue = '';
 							$bVertical = ($sDisplayStyle != 'radio_horizontal');
-							$sHTMLValue = $oPage->GetRadioButtons($aAllowedValues, $value, $iId, "attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}", $bMandatory, $bVertical, $sValidationField);
+							$sHTMLValue = $oPage->GetRadioButtons($aAllowedValues, $value, $iId, "attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}", $bMandatory, $bVertical, $sValidationSpan.$sReloadSpan);
 							$aEventsList[] ='change';
 							break;
 							
@@ -1946,13 +1995,13 @@ EOF
 								}
 								$sHTMLValue .= "<option value=\"$key\"$sSelected>$display_value</option>\n";
 							}
-							$sHTMLValue .= "</select>&nbsp;{$sValidationField}\n";
+							$sHTMLValue .= "</select>&nbsp;{$sValidationSpan}{$sReloadSpan}\n";
 							$aEventsList[] ='change';
 						}
 					}
 					else
 					{
-						$sHTMLValue = "<input title=\"$sHelpText\" type=\"text\" size=\"30\" maxlength=\"$iFieldSize\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" value=\"".htmlentities($sDisplayValue, ENT_QUOTES, 'UTF-8')."\" id=\"$iId\"/>&nbsp;{$sValidationField}";
+						$sHTMLValue = "<input title=\"$sHelpText\" type=\"text\" size=\"30\" maxlength=\"$iFieldSize\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" value=\"".htmlentities($sDisplayValue, ENT_QUOTES, 'UTF-8')."\" id=\"$iId\"/>&nbsp;{$sValidationSpan}{$sReloadSpan}";
 						$aEventsList[] ='keyup';
 						$aEventsList[] ='change';
 					}
@@ -2977,6 +3026,10 @@ EOF
 					$this->Set($sAttCode, $iValue);
 				}
 			}
+			elseif ($oAttDef->GetEditClass() == 'CustomFields')
+			{
+				$this->Set($sAttCode, $value);
+			}
 			else if (($oAttDef->GetEditClass() == 'LinkedSet') && !$oAttDef->IsIndirect() &&
 			          (($oAttDef->GetEditMode() == LINKSET_EDITMODE_INPLACE) || ($oAttDef->GetEditMode() == LINKSET_EDITMODE_ADDREMOVE)))
 			{
@@ -3089,6 +3142,10 @@ EOF
 			{
 				$value = $oAttDef->ReadValueFromPostedForm($sFormPrefix);
 			}
+			elseif ($oAttDef->GetEditClass() == 'CustomFields')
+			{
+				$value = $oAttDef->ReadValueFromPostedForm($this, $sFormPrefix);
+			}
 			else if (($oAttDef->GetEditClass() == 'LinkedSet') && !$oAttDef->IsIndirect() &&
 			         (($oAttDef->GetEditMode() == LINKSET_EDITMODE_INPLACE) || ($oAttDef->GetEditMode() == LINKSET_EDITMODE_ADDREMOVE)) )
 			{

+ 4 - 4
application/ui.extkeywidget.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2016 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -54,7 +54,7 @@
  * | |   +--------+    +-----+                    | |
  * | +--------------------------------------------+ |
  * +------------------------------------------------+
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2016 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -149,7 +149,7 @@ class UIExtKeyWidget
 				case 'radio':
 				case 'radio_horizontal':
 				case 'radio_vertical':
-				$sValidationField = "<span id=\"v_{$this->iId}\"></span>";
+				$sValidationField = "<span id=\"v_{$this->iId}\"></span><span id=\"fstatus_{$this->iId}\"></span>";
 				$sHTMLValue = '';
 				$bVertical = ($sDisplayStyle != 'radio_horizontal');
 				$bExtensions = false;
@@ -305,7 +305,7 @@ EOF
 		}
 		if (($sDisplayStyle == 'select') || ($sDisplayStyle == 'list'))
 		{
-			$sHTMLValue .= "<span id=\"v_{$this->iId}\"></span>";
+			$sHTMLValue .= "<span id=\"v_{$this->iId}\"></span><span id=\"fstatus_{$this->iId}\"></span>";
 		}
 		$sHTMLValue .= "</span>"; // end of no wrap
 		return $sHTMLValue;

+ 1 - 1
application/ui.passwordwidget.class.inc.php

@@ -58,7 +58,7 @@ class UIPasswordWidget
 		$sConfirmPasswordValue = $aPasswordValues ? $aPasswordValues['confirm'] : '*****';
 		$sChangedValue = (($sPasswordValue != '*****') || ($sConfirmPasswordValue != '*****')) ? 1 : 0;
 		$sHtmlValue = '';
-		$sHtmlValue = '<input type="password" maxlength="255" name="attr_'.$sCode.'[value]" id="'.$this->iId.'" value="'.htmlentities($sPasswordValue, ENT_QUOTES, 'UTF-8').'"/>&nbsp;<span class="form_validation" id="v_'.$this->iId.'"></span><br/>';
+		$sHtmlValue = '<input type="password" maxlength="255" name="attr_'.$sCode.'[value]" id="'.$this->iId.'" value="'.htmlentities($sPasswordValue, ENT_QUOTES, 'UTF-8').'"/>&nbsp;<span class="form_validation" id="v_'.$this->iId.'"></span><span id="fstatus_'.$this->iId.'"></span><br/>';
 		$sHtmlValue .= '<input type="password" maxlength="255" id="'.$this->iId.'_confirm" value="'.htmlentities($sConfirmPasswordValue, ENT_QUOTES, 'UTF-8').'" name="attr_'.$sCode.'[confirm]"/> '.Dict::S('UI:PasswordConfirm').' <input id="'.$this->iId.'_reset" type="button" value="'.Dict::S('UI:Button:ResetPassword').'" onClick="ResetPwd(\''.$this->iId.'\');">';
 		$sHtmlValue .= '<input type="hidden" id="'.$this->iId.'_changed" name="attr_'.$sCode.'[changed]" value="'.$sChangedValue.'"/>';
 

+ 307 - 55
core/attributedef.class.inc.php

@@ -31,6 +31,9 @@ require_once('ormstopwatch.class.inc.php');
 require_once('ormpassword.class.inc.php');
 require_once('ormcaselog.class.inc.php');
 require_once('htmlsanitizer.class.inc.php');
+require_once(APPROOT.'sources/autoload.php');
+require_once('customfieldshandler.class.inc.php');
+require_once('ormcustomfieldsvalue.class.inc.php');
 
 /**
  * MissingColumnException - sent if an attribute is being created but the column is missing in the row 
@@ -178,7 +181,6 @@ abstract class AttributeDefinition
 
 	private function ConsistencyCheck()
 	{
-
 		// Check that any mandatory param has been specified
 		//
 		$aExpectedParams = $this->ListExpectedParams();
@@ -192,7 +194,18 @@ abstract class AttributeDefinition
 				throw new Exception("ERROR missing parameter '$sParamName' in ".get_class($this)." declaration for class $sTargetClass ($sCodeInfo)");
 			}
 		}
-	} 
+	}
+
+	/**
+	 * Check the validity of the given value
+	 * @param DBObject $oHostObject
+	 * @param string An error if any, null otherwise
+	 */
+	public function CheckValue(DBObject $oHostObject, $value)
+	{
+		// todo: factorize here the cases implemented into DBObject
+		return true;
+	}
 
 	// table, key field, name field
 	public function ListDBJoins()
@@ -212,8 +225,9 @@ abstract class AttributeDefinition
 	public function IsExternalField() {return false;} 
 	public function IsWritable() {return false;} 
 	public function LoadInObject() {return true;}
+	public function LoadFromDB() {return true;}
 	public function AlwaysLoadInTables() {return $this->GetOptional('always_load_in_tables', false);}
-	public function GetValue($oHostObject){return null;} // must return the value if LoadInObject returns false
+	public function GetValue($oHostObject, $bOriginal = false){return null;} // must return the value if LoadInObject returns false
 	public function IsNullAllowed() {return true;} 
 	public function GetCode() {return $this->m_sCode;} 
 	public function GetMirrorLinkAttribute() {return null;}
@@ -417,7 +431,7 @@ abstract class AttributeDefinition
 		return call_user_func($sComputeFunc);
 	}
 	
-	abstract public function GetDefaultValue();
+	abstract public function GetDefaultValue(DBObject $oHostObject = null);
 
 	//
 	// To be overloaded in subclasses
@@ -503,7 +517,7 @@ abstract class AttributeDefinition
 	/**
 	 * List the available verbs for 'GetForTemplate'
 	 */	 
-	public static function EnumTemplateVerbs()
+	public function EnumTemplateVerbs()
 	{
 		return array(
 			'' => 'Plain text (unlocalized) representation',
@@ -692,21 +706,9 @@ class AttributeLinkedSet extends AttributeDefinition
 
 	public function GetValuesDef() {return $this->Get("allowed_values");} 
 	public function GetPrerequisiteAttributes($sClass = null) {return $this->Get("depends_on");}
-	public function GetDefaultValue($aArgs = array())
+	public function GetDefaultValue(DBObject $oHostObject = null)
 	{
-		// Note: so far, this feature is a prototype,
-		//       later, the argument 'this' should always be present in the arguments
-		//       
-		if (($this->IsParam('default_value')) && array_key_exists('this', $aArgs))
-		{
-			$aValues = $this->Get('default_value')->GetValues($aArgs);
-			$oSet = DBObjectSet::FromArray($this->Get('linked_class'), $aValues);
-			return $oSet;
-		}
-		else
-		{
-			return DBObjectSet::FromScratch($this->Get('linked_class'));
-		}
+		return DBObjectSet::FromScratch($this->Get('linked_class'));
 	}
 
 	public function GetTrackingLevel()
@@ -849,7 +851,7 @@ class AttributeLinkedSet extends AttributeDefinition
 	/**
 	 * List the available verbs for 'GetForTemplate'
 	 */	 
-	public static function EnumTemplateVerbs()
+	public function EnumTemplateVerbs()
 	{
 		return array(
 			'' => 'Plain text (unlocalized) representation',
@@ -1295,7 +1297,7 @@ class AttributeDBFieldVoid extends AttributeDefinition
 	public function IsScalar() {return true;} 
 	public function IsWritable() {return true;} 
 	public function GetSQLExpr()    {return $this->Get("sql");}
-	public function GetDefaultValue() {return $this->MakeRealValue("", null);}
+	public function GetDefaultValue(DBObject $oHostObject = null) {return $this->MakeRealValue("", $oHostObject);}
 	public function IsNullAllowed() {return false;}
 
 	// 
@@ -1368,7 +1370,7 @@ class AttributeDBField extends AttributeDBFieldVoid
 	{
 		return array_merge(parent::ListExpectedParams(), array("default_value", "is_null_allowed"));
 	}
-	public function GetDefaultValue() {return $this->MakeRealValue($this->Get("default_value"), null);}
+	public function GetDefaultValue(DBObject $oHostObject = null) {return $this->MakeRealValue($this->Get("default_value"), $oHostObject);}
 	public function IsNullAllowed() {return $this->Get("is_null_allowed");}
 }
 
@@ -1480,7 +1482,7 @@ class AttributeObjectKey extends AttributeDBFieldVoid
 	public function GetEditClass() {return "String";}
 	protected function GetSQLCol($bFullSpec = false) {return "INT(11)".($bFullSpec ? " DEFAULT 0" : "");}
 
-	public function GetDefaultValue() {return 0;}
+	public function GetDefaultValue(DBObject $oHostObject = null) {return 0;}
 	public function IsNullAllowed()
 	{
 		return $this->Get("is_null_allowed");
@@ -1854,9 +1856,9 @@ class AttributeClass extends AttributeString
 		parent::__construct($sCode, $aParams);
 	}
 
-	public function GetDefaultValue()
+	public function GetDefaultValue(DBObject $oHostObject = null)
 	{
-		$sDefault = parent::GetDefaultValue();
+		$sDefault = parent::GetDefaultValue($oHostObject);
 		if (!$this->IsNullAllowed() && $this->IsNull($sDefault))
 		{
 			// For this kind of attribute specifying null as default value
@@ -1953,7 +1955,7 @@ class AttributeFinalClass extends AttributeString
 	{
 		$this->m_sValue = $sValue;
 	}
-	public function GetDefaultValue()
+	public function GetDefaultValue(DBObject $oHostObject = null)
 	{
 		return $this->m_sValue;
 	}
@@ -2312,7 +2314,7 @@ class AttributeText extends AttributeString
 				{
 					$sClass = $aMatches[1];
 					$sName = $aMatches[2];
-					
+
 					if (MetaModel::IsValidClass($sClass))
 					{
 						$sClassLabel = MetaModel::GetName($sClass);
@@ -2357,7 +2359,7 @@ class AttributeText extends AttributeString
 				{
 					$sClassLabel = $aMatches[1];
 					$sName = $aMatches[2];
-					
+
 					if (!MetaModel::IsValidClass($sClassLabel))
 					{
 						$sClass = MetaModel::GetClassFromLabel($sClassLabel);
@@ -2548,8 +2550,8 @@ class AttributeCaseLog extends AttributeLongText
 			return (string) $value;
 		}
 	}
-		
-	public function GetDefaultValue() {return new ormCaseLog();}
+	
+	public function GetDefaultValue(DBObject $oHostObject = null) {return new ormCaseLog();}
 	public function Equals($val1, $val2) {return ($val1->GetText() == $val2->GetText());}
 	
 
@@ -2719,7 +2721,7 @@ class AttributeCaseLog extends AttributeLongText
 	/**
 	 * List the available verbs for 'GetForTemplate'
 	 */	 
-	public static function EnumTemplateVerbs()
+	public function EnumTemplateVerbs()
 	{
 		return array(
 			'' => 'Plain text representation of all the log entries',
@@ -3372,9 +3374,9 @@ class AttributeDateTime extends AttributeDBField
 	// This has been done at the time when itop was using TIMESTAMP columns,
 	// now that iTop is using DATETIME columns, it seems possible to have IsNullAllowed returning false... later when this is needed
 	public function IsNullAllowed() {return true;}
-	public function GetDefaultValue()
+	public function GetDefaultValue(DBObject $oHostObject = null)
 	{
-		$default = parent::GetDefaultValue();
+		$default = parent::GetDefaultValue($oHostObject);
 
 		if (!parent::IsNullAllowed())
 		{
@@ -3769,7 +3771,7 @@ class AttributeExternalKey extends AttributeDBFieldVoid
 	public function GetDisplayStyle() { return $this->GetOptional('display_style', 'select'); }
 	
 
-	public function GetDefaultValue() {return 0;}
+	public function GetDefaultValue(DBObject $oHostObject = null) {return 0;}
 	public function IsNullAllowed()
 	{
 		if (MetaModel::GetConfig()->Get('disable_mandatory_ext_keys'))
@@ -4163,10 +4165,10 @@ class AttributeExternalField extends AttributeDefinition
 		return $oExtAttDef->GetSQLExpr(); 
 	} 
 
-	public function GetDefaultValue()
+	public function GetDefaultValue(DBObject $oHostObject = null)
 	{
 		$oExtAttDef = $this->GetExtAttDef();
-		return $oExtAttDef->GetDefaultValue(); 
+		return $oExtAttDef->GetDefaultValue();
 	}
 	public function IsNullAllowed()
 	{
@@ -4308,8 +4310,8 @@ class AttributeBlob extends AttributeDefinition
 	public function IsDirectField() {return true;} 
 	public function IsScalar() {return true;} 
 	public function IsWritable() {return true;} 
-	public function GetDefaultValue() {return "";}
-	public function IsNullAllowed() {return $this->GetOptional("is_null_allowed", false);}
+	public function GetDefaultValue(DBObject $oHostObject = null) {return "";}
+	public function IsNullAllowed(DBObject $oHostObject = null) {return $this->GetOptional("is_null_allowed", false);}
 
 	public function GetEditValue($sValue, $oHostObj = null)
 	{
@@ -4569,7 +4571,7 @@ class AttributeStopWatch extends AttributeDefinition
 	public function IsDirectField() {return true;} 
 	public function IsScalar() {return true;} 
 	public function IsWritable() {return false;} 
-	public function GetDefaultValue() {return $this->NewStopWatch();}
+	public function GetDefaultValue(DBObject $oHostObject = null) {return $this->NewStopWatch();}
 
 	public function GetEditValue($value, $oHostObj = null)
 	{
@@ -5186,9 +5188,21 @@ class AttributeSubItem extends AttributeDefinition
 	public function IsDirectField() {return true;} 
 	public function IsScalar() {return true;} 
 	public function IsWritable() {return false;} 
-	public function GetDefaultValue() {return null;}
+	public function GetDefaultValue(DBObject $oHostObject = null) {return null;}
 //	public function IsNullAllowed() {return false;}
-	public function LoadInObject() {return false;} // if this verb returns true, then GetValue must be implemented
+
+	public function LoadInObject() {return false;} // if this verb returns false, then GetValue must be implemented
+
+	/**
+	 * Used by DBOBject::Get()
+	 */
+	public function GetValue($oHostObject, $bOriginal = false)
+	{
+		$oParent = $this->GetTargetAttDef();
+		$parentValue = $oHostObject->GetStrict($oParent->GetCode());
+		$res = $oParent->GetSubItemValue($this->Get('item_code'), $parentValue, $oHostObject);
+		return $res;
+	}
 
 	// 
 //	protected function ScalarToSQL($value) {return $value;} // format value as a valuable SQL literal (quoted outside)
@@ -5237,16 +5251,6 @@ class AttributeSubItem extends AttributeDefinition
 		return $res;
 	}
 
-	/**
-	 * Used by DBOBject::Get()	
-	 */	
-	public function GetValue($parentValue, $oHostObject = null)
-	{
-		$oParent = $this->GetTargetAttDef();
-		$res = $oParent->GetSubItemValue($this->Get('item_code'), $parentValue, $oHostObject);
-		return $res;
-	}
-
 	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
 	{
 		$oParent = $this->GetTargetAttDef();
@@ -5303,7 +5307,7 @@ class AttributeOneWayPassword extends AttributeDefinition
 	public function IsDirectField() {return true;} 
 	public function IsScalar() {return true;} 
 	public function IsWritable() {return true;} 
-	public function GetDefaultValue() {return "";}
+	public function GetDefaultValue(DBObject $oHostObject = null) {return "";}
 	public function IsNullAllowed() {return $this->GetOptional("is_null_allowed", false);}
 
 	// Facilitate things: allow the user to Set the value from a string or from an ormPassword (already encrypted)
@@ -5676,7 +5680,7 @@ class AttributeComputedFieldVoid extends AttributeDefinition
 	public function IsScalar() {return true;} 
 	public function IsWritable() {return false;} 
 	public function GetSQLExpr()    {return null;}
-	public function GetDefaultValue() {return $this->MakeRealValue("", null);}
+	public function GetDefaultValue(DBObject $oHostObject = null) {return $this->MakeRealValue("", $oHostObject);}
 	public function IsNullAllowed() {return false;}
 
 	// 
@@ -5827,7 +5831,7 @@ class AttributeFriendlyName extends AttributeComputedFieldVoid
 	{
 		$this->m_sValue = $sValue;
 	}
-	public function GetDefaultValue()
+	public function GetDefaultValue(DBObject $oHostObject = null)
 	{
 		return $this->m_sValue;
 	}
@@ -5913,7 +5917,7 @@ class AttributeRedundancySettings extends AttributeDBField
 		return 20;
 	}
 
-	public function GetDefaultValue($aArgs = array())
+	public function GetDefaultValue(DBObject $oHostObject = null)
 	{
 		$sRet = 'disabled';
 		if ($this->Get('enabled'))
@@ -6268,3 +6272,251 @@ class AttributeRedundancySettings extends AttributeDBField
 		return $sRet;
 	}
 }
+
+/**
+ * Custom fields managed by an external implementation
+ *
+ * @package     iTopORM
+ */
+class AttributeCustomFields extends AttributeDefinition
+{
+	static public function ListExpectedParams()
+	{
+		return array_merge(parent::ListExpectedParams(), array("handler_class"));
+	}
+
+	public function GetEditClass() {return "CustomFields";}
+	public function IsWritable() {return true;}
+	public function LoadFromDB() {return false;} // See ReadValue...
+
+	public function GetDefaultValue(DBObject $oHostObject = null)
+	{
+		return new ormCustomFieldsValue($oHostObject, $this->GetCode());
+	}
+
+	public function GetBasicFilterOperators() {return array();}
+	public function GetBasicFilterLooseOperator() {return '';}
+	public function GetBasicFilterSQLExpr($sOpCode, $value) {return '';}
+
+	/**
+	 * @param DBObject $oHostObject
+	 * @param ormCustomFieldsValue|null $oValue
+	 * @return CustomFieldsHandler
+	 */
+	public function GetHandler(DBObject $oHostObject, ormCustomFieldsValue $oValue = null)
+	{
+		$sHandlerClass = $this->Get('handler_class');
+		$oHandler = new $sHandlerClass($oHostObject, $this->GetCode());
+		if (!is_null($oValue))
+		{
+			$oHandler->SetCurrentValues($oValue->GetValues());
+		}
+		return $oHandler;
+	}
+
+	public function GetPrerequisiteAttributes($sClass = null)
+	{
+		$sHandlerClass = $this->Get('handler_class');
+		return $sHandlerClass::GetPrerequisiteAttributes($sClass);
+	}
+
+	public function GetEditValue($sValue, $oHostObj = null)
+	{
+		return 'GetEditValueNotImplemented for '.get_class($this);
+	}
+
+	/**
+	 * Makes the string representation out of the values given by the form defined in GetDisplayForm
+	 */
+	public function ReadValueFromPostedForm($oHostObject, $sFormPrefix)
+	{
+		$aRawData = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$this->GetCode()}", '{}', 'raw_data'), true);
+		return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aRawData);
+	}
+
+	public function MakeRealValue($proposedValue, $oHostObject)
+	{
+		if (is_object($proposedValue) && ($proposedValue instanceof ormCustomFieldsValue))
+		{
+			return $proposedValue;
+		}
+		elseif (is_string($proposedValue))
+		{
+			$aValues = json_decode($proposedValue, true);
+			return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aValues);
+		}
+		throw new Exception('Unexpected type for the value of a custom fields attribute: '.gettype($proposedValue));
+	}
+
+	/**
+	 * Override to build the relevant form field
+	 *
+	 * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the $oFormField is passed, MakeFormField behaves more like a Prepare.
+	 */
+	public function MakeFormField(DBObject $oObject, $oFormField = null)
+	{
+		if ($oFormField === null)
+		{
+			$oField = new Combodo\iTop\Form\Field\SubFormField($this->GetCode(), $sParentFormId);
+			$oField->SetForm($this->GetForm($oObject));
+		}
+		parent::MakeFormField($oObject, $oFormField);
+
+		return $oFormField;
+	}
+
+	/**
+	 * @param DBObject $oHostObject
+	 * @return Combodo\iTop\Form\Form
+	 */
+	public function GetForm(DBObject $oHostObject, $sFormPrefix = null)
+	{
+		$oHandler = $this->GetHandler($oHostObject, $oHostObject->Get($this->GetCode()));
+		$sFormId = is_null($sFormPrefix) ? 'cf_'.$this->GetCode() : $sFormPrefix.'_cf_'.$this->GetCode();
+		$oHandler->BuildForm($sFormId);
+		return $oHandler->GetForm();
+	}
+
+	/**
+	 * Read the data from where it has been stored. This verb must be implemented as soon as LoadFromDB returns false and LoadInObject returns true
+	 * @param $oHostObject
+	 * @return ormCustomFieldsValue
+	 */
+	public function ReadValue($oHostObject)
+	{
+		$oHandler = $this->GetHandler($oHostObject);
+		$aValues = $oHandler->ReadValues();
+		$oRet = new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aValues);
+		return $oRet;
+	}
+
+	/**
+	 * Record the data (currently in the processing of recording the host object)
+	 * It is assumed that the data has been checked prior to calling Write()
+	 * @param DBObject $oHostObject
+	 * @param ormCustomFieldsValue|null $oValue (null is the default value)
+	 */
+	public function WriteValue(DBObject $oHostObject, ormCustomFieldsValue $oValue = null)
+	{
+		$oHandler = $this->GetHandler($oHostObject, $oHostObject->Get($this->GetCode()));
+		if (is_null($oValue))
+		{
+			$aValues = array();
+		}
+		else
+		{
+			$aValues = $oValue->GetValues();
+		}
+		return $oHandler->WriteValues($aValues);
+	}
+
+	/**
+	 * Check the validity of the data
+	 * @param DBObject $oHostObject
+	 * @param $value
+	 * @return bool|string true or error message
+	 */
+	public function CheckValue(DBObject $oHostObject, $value)
+	{
+		try
+		{
+			$oHandler = $this->GetHandler($oHostObject, $value);
+			$oHandler->BuildForm();
+			$oForm = $oHandler->GetForm();
+			$oForm->Validate();
+			if ($oForm->GetValid())
+			{
+				$ret = true;
+			}
+			else
+			{
+				$aMessages = array();
+				foreach ($oForm->GetErrorMessages() as $sFieldId => $aFieldMessages)
+				{
+					$aMessages[] = $sFieldId.': '.implode(', ', $aFieldMessages);
+				}
+				$ret = 'Invalid value: '.implode(', ', $aMessages);
+			}
+		}
+		catch (Exception $e)
+		{
+			$ret = $e->getMessage();
+		}
+		return $ret;
+	}
+
+	/**
+	 * Cleanup data upon object deletion (object id still available here)
+	 * @param DBObject $oHostObject
+	 */
+	public function DeleteValue(DBObject $oHostObject)
+	{
+		$oHandler = $this->GetHandler($oHostObject, $oHostObject->Get($this->GetCode()));
+		return $oHandler->DeleteValues();
+	}
+
+	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
+	{
+		return $value->GetAsHTML($bLocalize);
+	}
+
+	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
+	{
+		return $value->GetAsXML($bLocalize);
+	}
+
+	public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false)
+	{
+		return $value->GetAsCSV($sSeparator, $sTextQualifier, $bLocalize, $bConvertToPlainText);
+	}
+
+	/**
+	 * List the available verbs for 'GetForTemplate'
+	 */
+	public function EnumTemplateVerbs()
+	{
+		$sHandlerClass = $this->Get('handler_class');
+		return $sHandlerClass::EnumTemplateVerbs();
+	}
+
+	/**
+	 * Get various representations of the value, for insertion into a template (e.g. in Notifications)
+	 * @param $value mixed The current value of the field
+	 * @param $sVerb string The verb specifying the representation of the value
+	 * @param $oHostObject DBObject The object
+	 * @param $bLocalize bool Whether or not to localize the value
+	 */
+	public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true)
+	{
+		return $value->GetForTemplate($sVerb, $bLocalize);
+	}
+
+	public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null)
+	{
+		return null;
+	}
+
+	/**
+	 * Helper to get a value that will be JSON encoded
+	 * The operation is the opposite to FromJSONToValue
+	 */
+	public function GetForJSON($value)
+	{
+		return null;
+	}
+
+	/**
+	 * Helper to form a value, given JSON decoded data
+	 * The operation is the opposite to GetForJSON
+	 */
+	public function FromJSONToValue($json)
+	{
+		return null;
+	}
+
+	public function Equals($val1, $val2)
+	{
+		return $val1->Equals($val2);
+	}
+}
+

+ 2 - 6
core/cmdbobject.class.inc.php

@@ -302,14 +302,10 @@ abstract class CMDBObject extends DBObject
 			{
 				// Stop watches - record changes for sub items only (they are visible, the rest is not visible)
 				//
-				if (is_null($original))
-				{
-					$original = new OrmStopWatch();
-				}
 				foreach ($oAttDef->ListSubItems() as $sSubItemAttCode => $oSubItemAttDef)
 				{
-					$item_value = $oSubItemAttDef->GetValue($value);
-					$item_original = $oSubItemAttDef->GetValue($original);
+					$item_value = $oSubItemAttDef->GetValue($this);
+					$item_original = $oSubItemAttDef->GetValue($this, true);
 
 					if ($item_value != $item_original)
 					{

+ 127 - 0
core/customfieldshandler.class.inc.php

@@ -0,0 +1,127 @@
+<?php
+// Copyright (C) 2016 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+use Combodo\iTop\Form\Form;
+use Combodo\iTop\Form\FormManager;
+
+/**
+ * Base class to implement a handler for AttributeCustomFields
+ *
+ * @copyright   Copyright (C) 2016 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+abstract class CustomFieldsHandler
+{
+	protected $oHostObject;
+	protected $sAttCode;
+	protected $aValues;
+	protected $oForm;
+
+	/**
+	 * This constructor's prototype must be frozen.
+	 * Any specific behavior must be implemented in BuildForm()
+	 *
+	 * @param DBObject $oHostObject
+	 * @param $sAttCode
+	 */
+	final public function __construct(DBObject $oHostObject, $sAttCode)
+	{
+		$this->oHostObject = $oHostObject;
+		$this->sAttCode = $sAttCode;
+		$this->aValues = null;
+	}
+
+	abstract public function BuildForm($sFormId);
+
+	/**
+	 *
+	 * @return \Combodo\iTop\Form\Form
+	 */
+	public function GetForm()
+	{
+		return $this->oForm;
+	}
+
+	public function SetCurrentValues($aValues)
+	{
+		$this->aValues = $aValues;
+	}
+
+	static public function GetPrerequisiteAttributes($sClass = null)
+	{
+		return array();
+	}
+
+	/**
+	 * List the available verbs for 'GetForTemplate'
+	 */
+	static public function EnumTemplateVerbs()
+	{
+		return array();
+	}
+
+	/**
+	 * Get various representations of the value, for insertion into a template (e.g. in Notifications)
+	 * @param $aValues array The current values
+	 * @param $sVerb string The verb specifying the representation of the value
+	 * @param $bLocalize bool Whether or not to localize the value
+	 * @return string
+	 */
+	abstract public function GetForTemplate($aValues, $sVerb, $bLocalize = true);
+
+	/**
+	 * @param $aValues
+	 * @param bool|true $bLocalize
+	 * @return mixed
+	 */
+	abstract public function GetAsHTML($aValues, $bLocalize = true);
+
+	/**
+	 * @param $aValues
+	 * @param bool|true $bLocalize
+	 * @return mixed
+	 */
+	abstract public function GetAsXML($aValues, $bLocalize = true);
+
+	/**
+	 * @param $aValues
+	 * @param string $sSeparator
+	 * @param string $sTextQualifier
+	 * @param bool|true $bLocalize
+	 * @return mixed
+	 */
+	abstract public function GetAsCSV($aValues, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true);
+
+	/**
+	 * @return array Associative array id => value
+	 */
+	abstract public function ReadValues();
+
+	/**
+	 * Record the data (currently in the processing of recording the host object)
+	 * It is assumed that the data has been checked prior to calling Write()
+	 * @param array Associative array id => value
+	 */
+	abstract public function WriteValues($aValues);
+
+	/**
+	 * Cleanup data upon object deletion (object id still available here)
+	 */
+	abstract public function DeleteValues();
+}

+ 48 - 14
core/dbobject.class.php

@@ -116,7 +116,7 @@ abstract class DBObject implements iDisplay
 		// set default values
 		foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef)
 		{
-			$this->m_aCurrValues[$sAttCode] = $oAttDef->GetDefaultValue();
+			$this->m_aCurrValues[$sAttCode] = $oAttDef->GetDefaultValue($this);
 			$this->m_aOrigValues[$sAttCode] = null;
 			if ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName))
 			{
@@ -291,16 +291,30 @@ abstract class DBObject implements iDisplay
 
 			if (!$oAttDef->LoadInObject()) continue;
 
-			// Note: we assume that, for a given attribute, if it can be loaded,
-			// then one column will be found with an empty suffix, the others have a suffix
-			// Take care: the function isset will return false in case the value is null,
-			// which is something that could happen on open joins
-			$sAttRef = $sClassAlias.$sAttCode;
+			unset($value);
+			$bIsDefined = false;
+			if ($oAttDef->LoadFromDB())
+			{
+				// Note: we assume that, for a given attribute, if it can be loaded,
+				// then one column will be found with an empty suffix, the others have a suffix
+				// Take care: the function isset will return false in case the value is null,
+				// which is something that could happen on open joins
+				$sAttRef = $sClassAlias.$sAttCode;
 
-			if (array_key_exists($sAttRef, $aRow))
+				if (array_key_exists($sAttRef, $aRow))
+				{
+					$value = $oAttDef->FromSQLToValue($aRow, $sAttRef);
+					$bIsDefined = true;
+				}
+			}
+			else
 			{
-				$value = $oAttDef->FromSQLToValue($aRow, $sAttRef);
+				$value = $oAttDef->ReadValue($this);
+				$bIsDefined = true;
+			}
 
+			if ($bIsDefined)
+			{
 				$this->m_aCurrValues[$sAttCode] = $value;
 				if (is_object($value))
 				{
@@ -380,7 +394,7 @@ abstract class DBObject implements iDisplay
 				{
 					if (($oDef->IsExternalField() || ($oDef instanceof AttributeFriendlyName)) && ($oDef->GetKeyAttCode() == $sAttCode))
 					{
-						$this->m_aCurrValues[$sCode] = $oDef->GetDefaultValue();
+						$this->m_aCurrValues[$sCode] = $oDef->GetDefaultValue($this);
 						unset($this->m_aLoadedAtt[$sCode]);
 					}
 				}
@@ -492,9 +506,7 @@ abstract class DBObject implements iDisplay
 
 		if (!$oAttDef->LoadInObject())
 		{
-			$sParentAttCode = $oAttDef->GetParentAttCode();
-			$parentValue = $this->GetStrict($sParentAttCode);
-			$value = $oAttDef->GetValue($parentValue, $this);
+			$value = $oAttDef->GetValue($this);
 		}
 		else
 		{
@@ -550,7 +562,7 @@ abstract class DBObject implements iDisplay
 							}
 							else
 							{
-								$this->m_aCurrValues[$sCode] = $oDef->GetDefaultValue();
+								$this->m_aCurrValues[$sCode] = $oDef->GetDefaultValue($this);
 							}
 							$this->m_aLoadedAtt[$sCode] = true;
 						}
@@ -1109,6 +1121,10 @@ abstract class DBObject implements iDisplay
 				return "Wrong format [$toCheck]";
 			}
 		}
+		else
+		{
+			return $oAtt->CheckValue($this, $toCheck);
+		}
 		return true;
 	}
 	
@@ -1405,6 +1421,17 @@ abstract class DBObject implements iDisplay
 		}
 	}
 
+	// used both by insert/update
+	private function WriteExternalAttributes()
+	{
+		foreach (MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
+		{
+			if (!$oAttDef->LoadInObject()) continue;
+			if ($oAttDef->LoadFromDB()) continue;
+			$oAttDef->WriteValue($this, $this->m_aCurrValues[$sAttCode]);
+		}
+	}
+
 	// Note: this is experimental - it was designed to speed up the setup of iTop
 	// Known limitations:
 	//   - does not work with multi-table classes (issue with the unique id to maintain in several tables)
@@ -1591,6 +1618,8 @@ abstract class DBObject implements iDisplay
 		}
 
 		$this->DBWriteLinks();
+		$this->WriteExternalAttributes();
+
 		$this->m_bIsInDB = true;
 		$this->m_bDirty = false;
 
@@ -1878,6 +1907,7 @@ abstract class DBObject implements iDisplay
 			}
 
 			$this->DBWriteLinks();
+			$this->WriteExternalAttributes();
 			$this->m_bDirty = false;
 
 			$this->AfterUpdate();
@@ -1981,6 +2011,10 @@ abstract class DBObject implements iDisplay
 					}
 					MetaModel::HKReplugBranch($iNewLeft, $iNewLeft + $iDelta - 1, $oAttDef, $sTable);
 				}
+				elseif (!$oAttDef->LoadFromDB())
+				{
+					$oAttDef->DeleteValue($this);
+				}
 			}
 
 			foreach(MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL) as $sParentClass)
@@ -2222,7 +2256,7 @@ abstract class DBObject implements iDisplay
 	public function Reset($sAttCode)
 	{
 		$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
-		$this->Set($sAttCode, $oAttDef->GetDefaultValue());
+		$this->Set($sAttCode, $oAttDef->GetDefaultValue($this));
 		return true;
 	}
 

+ 101 - 0
core/ormcustomfieldsvalue.class.inc.php

@@ -0,0 +1,101 @@
+<?php
+// Copyright (C) 2016 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+
+/**
+ * Base class to hold the value managed by CustomFieldsHandler
+ *
+ * @copyright   Copyright (C) 2016 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+class ormCustomFieldsValue
+{
+	protected $oHostObject;
+	protected $sAttCode;
+	protected $aCurrentValues;
+
+	/**
+	 * @param DBObject $oHostObject
+	 * @param $sAttCode
+	 */
+	public function __construct(DBObject $oHostObject, $sAttCode, $aCurrentValues = null)
+	{
+		$this->oHostObject = $oHostObject;
+		$this->sAttCode = $sAttCode;
+		$this->aCurrentValues = $aCurrentValues;
+	}
+
+	public function GetValues()
+	{
+		return $this->aCurrentValues;
+	}
+
+	/**
+	 * Wrapper used when the only thing you have is the value...
+	 * @return \Combodo\iTop\Form\Form
+	 */
+	public function GetForm()
+	{
+		$oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode);
+		return $oAttDef->GetForm($this->oHostObject);
+	}
+
+	public function GetAsHTML($bLocalize = true)
+	{
+		$oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode);
+		$oHandler = $oAttDef->GetHandler($this->oHostObject, $this);
+		return $oHandler->GetAsHTML($this->aCurrentValues, $bLocalize);
+	}
+
+	public function GetAsXML($bLocalize = true)
+	{
+		$oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode);
+		$oHandler = $oAttDef->GetHandler($this->oHostObject, $this);
+		return $oHandler->GetAsXML($this->aCurrentValues, $bLocalize);
+	}
+
+	public function GetAsCSV($sSeparator = ',', $sTextQualifier = '"', $bLocalize = true)
+	{
+		$oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode);
+		$oHandler = $oAttDef->GetHandler($this->oHostObject, $this);
+		return $oHandler->GetAsCSV($this->aCurrentValues, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true);
+	}
+
+	/**
+	 * Get various representations of the value, for insertion into a template (e.g. in Notifications)
+	 * @param $value mixed The current value of the field
+	 * @param $sVerb string The verb specifying the representation of the value
+	 * @param $bLocalize bool Whether or not to localize the value
+	 */
+	public function GetForTemplate($sVerb, $bLocalize = true)
+	{
+		$oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode);
+		$oHandler = $oAttDef->GetHandler($this->oHostObject, $this);
+		return 'template...verb='.$sVerb.' sur "'.json_encode($this->aCurrentValues).'"';
+	}
+
+	/**
+	 * @param ormCustomFieldsValue $fellow
+	 * @return bool
+	 */
+	public function Equals(ormCustomFieldsValue $oReference)
+	{
+		return (json_encode($this->aCurrentValues) === json_encode($oReference->aCurrentValues));
+	}
+}

+ 115 - 0
js/console_form_handler.js

@@ -0,0 +1,115 @@
+// Copyright (C) 2010-2016 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+//iTop Form handler
+;
+$(function()
+{
+    // the widget definition, where 'itop' is the namespace,
+    // 'consoleform_handler' the widget name
+    $.widget( 'itop.console_form_handler', $.itop.form_handler,
+    {
+        // default options
+        options:
+        {
+            wizard_helper_var_name: '', // Name of the global variable pointing to the wizard helper
+            custom_field_attcode: ''
+        },
+
+        // the constructor
+        _create: function()
+        {
+            var me = this;
+            
+            this.element
+            .addClass('console_form_handler');
+
+            this.options.oWizardHelper = window[this.options.wizard_helper_var_name];
+
+            this._super();
+        },
+   
+        // events bound via _bind are removed automatically
+        // revert other modifications here
+        _destroy: function()
+        {
+            this.element
+            .removeClass('console_form_handler');
+            this._super();
+        },
+        _onUpdateFields: function(event, data)
+        {
+            var me = this;
+            var sFormPath = data.form_path;
+            var sUpdateUrl = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php';
+
+            $(this.element.find('[data-form-path="' + sFormPath + '"]')).block({message:''});
+            $.post(
+                sUpdateUrl,
+                {
+                    operation: 'custom_fields_update',
+                    attcode: this.options.custom_field_attcode,
+                    //current_values: this.getCurrentValues(),
+                    requested_fields: data.requested_fields,
+                    form_path: sFormPath,
+                    json_obj: this.options.oWizardHelper.UpdateWizardToJSON()
+                },
+                function(data){
+                    me._onUpdateSuccess(data, sFormPath);
+                }
+            )
+            .fail(function(data){ me._onUpdateFailure(data, sFormPath); })
+            .always(function(data){
+                me.alignColumns();
+                $(me.element.find('[data-form-path="' + sFormPath + '"]')).unblock();
+                me._onUpdateAlways(data, sFormPath);
+            });
+        },
+        // On initialization or update
+        alignColumns: function()
+        {
+            var iMaxWidth = 0;
+            var oLabels = $(this.element.find('td.form-field-label'));
+            // Reset the width to the automatic (original) value
+            oLabels.width('');
+            oLabels.each(function() {
+                iMaxWidth = Math.max(iMaxWidth, $(this).width());
+            });
+            oLabels.width(iMaxWidth);
+        },
+        // Intended for overloading in derived classes
+        _onSubmitClick: function()
+        {
+        },
+        // Intended for overloading in derived classes
+        _onCancelClick: function()
+        {
+        },
+        // Intended for overloading in derived classes
+        _onUpdateFailure: function(data)
+        {
+        },
+        // Intended for overloading in derived classes
+        _disableFormBeforeLoading: function()
+        {
+        },
+        // Intended for overloading in derived classes
+        _enableFormAfterLoading: function()
+        {
+        },
+    });
+});

+ 4 - 10
js/extkeywidget.js

@@ -1,4 +1,4 @@
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2016 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -28,7 +28,6 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
 	this.ajax_request = null;
 	this.bSelectMode = bSelectMode; // true if the edited field is a SELECT, false if it's an autocomplete
 	this.bSearchMode = bSearchMode; // true if selecting a value in the context of a search form
-	this.v_html = '';
 	var me = this;
 	
 	this.Init = function()
@@ -55,8 +54,7 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
 		// Query the server to get the form to search for target objects
 		if (me.bSelectMode)
 		{
-			me.v_html = $('#v_'+me.id).html();
-			$('#v_'+me.id).html('<img src="../images/indicator.gif" />');
+			$('#fstatus_'+me.id).html('<img src="../images/indicator.gif" />');
 		}
 		else
 		{
@@ -284,8 +282,7 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
 		// Query the server to get the form to create a target object
 		if (me.bSelectMode)
 		{
-			me.v_html = $('#v_'+me.id).html();
-			$('#v_'+me.id).html('<img src="../images/indicator.gif" />');
+			$('#fstatus_'+me.id).html('<img src="../images/indicator.gif" />');
 		}
 		else
 		{
@@ -336,7 +333,6 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
 	{
 		if (me.bSelectMode)
 		{
-			$('#v_'+me.id).html(me.v_html);
 		}
 		else
 		{
@@ -446,8 +442,7 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
 	
 		if (me.bSelectMode)
 		{
-			me.v_html = $('#v_'+me.id).html();
-			$('#v_'+me.id).html('<img src="../images/indicator.gif" />');
+			$('#fstatus_'+me.id).html('<img src="../images/indicator.gif" />');
 		}
 		else
 		{
@@ -501,7 +496,6 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
 	{
 		if (me.bSelectMode)
 		{
-			$('#v_'+me.id).html(me.v_html);
 		}
 		else
 		{

+ 25 - 7
js/field_set.js

@@ -1,3 +1,20 @@
+// Copyright (C) 2010-2016 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
 //iTop Field set
 //Used by itop.form_handler and itop.subform_field to list their fields
 ;
@@ -36,15 +53,15 @@ $(function()
 			
 			this.element
 			.bind('field_change', function(oEvent, oData){
-				console.log('field_set: field_change');
+				//console.log('field_set: field_change');
 				me._onFieldChange(oEvent, oData);
 			})
 			.bind('update_form', function(oEvent, oData){
-				console.log('field_set: update_form');
+				//console.log('field_set: update_form');
 				me._onUpdateForm(oEvent, oData);
 			})
 			.bind('get_current_values', function(oEvent, oData){
-				console.log('field_set: get_current_values');
+				//console.log('field_set: get_current_values');
 				return me._onGetCurrentValues(oEvent, oData);
 			})
 			.bind('validate', function(oEvent, oData){
@@ -52,7 +69,7 @@ $(function()
 				{
 					oData = {};
 				}
-				console.log('field_set: validate');
+				//console.log('field_set: validate');
 				return me._onValidate(oEvent, oData);
 			});
 
@@ -175,8 +192,9 @@ $(function()
 			}
 
 			// Adding code to the dom
-			this.options.script_element.append('\n\n// Appended by update on ' + Date() + '\n' + this.buildData.script_code);
-			this.options.style_element.append('\n\n// Appended by update on ' + Date() + '\n' + this.buildData.style_code);
+			// Note : We use text() instead of append(), otherwise the code will be interpreted as DOM tags (text + <img /> + ...) and can break some browsers
+			this.options.script_element.text( this.options.script_element.text() + '\n\n// Appended by update on ' + Date() + '\n' + this.buildData.script_code);
+			this.options.style_element.text( this.options.style_element.text() + '\n\n// Appended by update on ' + Date() + '\n' + this.buildData.style_code);
 
 			// Evaluating script code as adding it to dom did not executed it (only script from update !)
 			eval(this.buildData.script_code);
@@ -291,7 +309,7 @@ $(function()
 				var oField = this.options.fields_list[i];
 				if(oField.id === undefined)
 				{
-					console.log('Field set : An field must have at least an id property.');
+					console.log('Field set : A field must have at least an id property.');
 					return false;
 				}
 

+ 26 - 0
js/forms-json-utils.js

@@ -1,3 +1,20 @@
+// Copyright (C) 2010-2016 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
 // ID of the (hidden) form field used to store the JSON representation of the
 // object being edited in this page
 var sJsonFieldId = 'json_object';
@@ -417,6 +434,15 @@ function ValidateRedundancySettings(sFieldId, sFormId)
 	return bValid;
 }
 
+//Special validation function for custom fields
+function ValidateCustomFields(sFieldId, sFormId)
+{
+	var oFieldSet = $('#'+sFieldId+'_console_form').console_form_handler('option', 'field_set');
+    bValid = oFieldSet.triggerHandler('validate');
+	ReportFieldValidationStatus(sFieldId, sFormId, bValid, '');
+	return bValid;
+}
+
 // Manage a 'duration' field
 function UpdateDuration(iId)
 {

+ 20 - 1
js/wizardhelper.js

@@ -1,3 +1,20 @@
+// Copyright (C) 2010-2016 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
 // Wizard Helper JavaScript class to communicate with the WizardHelper PHP class
 
 if (!Array.prototype.indexOf) // Emulation of the indexOf function for IE and old browsers
@@ -151,6 +168,7 @@ function WizardHelper(sClass, sFormPrefix, sState)
 		   { operation: 'wizard_helper', json_obj: this.ToJSON() },
 			function(html){
 				$('#ajax_content').html(html);
+				$('.blockUI').parent().unblock();
 				//console.log('data received:', oWizardHelper);
 				//oWizardHelper.FromJSON(json_data);
 				//oWizardHelper.UpdateFields(); // Is done directly in the html provided by ajax.render.php
@@ -191,7 +209,8 @@ function WizardHelper(sClass, sFormPrefix, sState)
 		{
 			sAttCode = aFieldNames[index];
 			sFieldId = this.GetFieldId(sAttCode);
-			$('#v_'+sFieldId).html('<img src="../images/indicator.gif" />');
+			$('#fstatus_'+sFieldId).html('<img src="../images/indicator.gif" />');
+			$('#field_'+sFieldId).closest('td').block({message:''});
 			this.RequestAllowedValues(sAttCode);
 			index++;
 		}

+ 25 - 3
pages/ajax.render.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2015 Combodo SARL
+// Copyright (C) 2010-2016 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Handles various ajax requests
  *
- * @copyright   Copyright (C) 2010-2015 Combodo SARL
+ * @copyright   Copyright (C) 2010-2016 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -563,7 +563,7 @@ try
 		foreach($oWizardHelper->GetFieldsForDefaultValue() as $sAttCode)
 		{
 			$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
-			$defaultValue = $oAttDef->GetDefaultValue();
+			$defaultValue = $oAttDef->GetDefaultValue($oObj);
 			$oWizardHelper->SetDefaultValue($sAttCode, $defaultValue);
 			$oObj->Set($sAttCode, $defaultValue);
 		}
@@ -2448,6 +2448,28 @@ EOF
 				}
 			}
 			break;
+
+		case 'custom_fields_update':
+			$oPage->SetContentType('application/json');
+			$sAttCode = utils::ReadParam('attcode', '');
+			$aRequestedFields = utils::ReadParam('requested_fields', array());
+			$sRequestedFieldsFormPath = utils::ReadParam('form_path', '');
+			$sJson = utils::ReadParam('json_obj', '', false, 'raw_data');
+
+			$oWizardHelper = WizardHelper::FromJSON($sJson);
+			$oObj = $oWizardHelper->GetTargetObject();
+
+			$oOrmCustomFieldValue = $oObj->Get($sAttCode);
+			$oForm = $oOrmCustomFieldValue->GetForm();
+			$oSubForm = $oForm->FindSubForm($sRequestedFieldsFormPath);
+			$oRenderer = new \Combodo\iTop\Renderer\Console\ConsoleFormRenderer($oSubForm);
+			$aRenderRes = $oRenderer->Render($aRequestedFields);
+
+			$aResult = array();
+			$aResult['form']['updated_fields'] = $aRenderRes;
+			$oPage->add(json_encode($aResult));
+			break;
+
 		default:
 		$oPage->p("Invalid query.");
 	}

+ 4 - 0
setup/compiler.class.inc.php

@@ -1206,6 +1206,10 @@ EOF;
 					$aParameters['min_up_mode'] = $this->GetMandatoryPropString($oField, 'min_up_mode');
 					$aParameters['min_up_type'] = $this->GetMandatoryPropString($oField, 'min_up_type');
 				}
+				elseif ($sAttType == 'AttributeCustomFields')
+				{
+					$aParameters['handler_class'] = $this->GetMandatoryPropString($oField, 'handler_class');
+				}
 				else
 				{
 					$aParameters['allowed_values'] = 'null'; // or "new ValueSetEnum('SELECT xxxx')"

+ 10 - 1
sources/form/field/subformfield.class.inc.php

@@ -61,7 +61,7 @@ class SubFormField extends Field
 	 */
 	public function Validate()
 	{
-		$this->oForm->Validate();
+		return $this->oForm->Validate();
 	}
 
 	/**
@@ -106,4 +106,13 @@ class SubFormField extends Field
 		$this->oForm->SetCurrentValues($value);
 		return $this;
 	}
+
+	/**
+	 * @param $sFormPath
+	 * @return Form|null
+	 */
+	public function FindSubForm($sFormPath)
+	{
+		return $this->oForm->FindSubForm($sFormPath);
+	}
 }

+ 83 - 57
sources/form/form.class.inc.php

@@ -19,6 +19,7 @@
 
 namespace Combodo\iTop\Form;
 
+use Combodo\iTop\Form\Field\SubFormField;
 use \Exception;
 use \Dict;
 use \Combodo\iTop\Form\Field\Field;
@@ -312,83 +313,108 @@ class Form
 	 * @param string $sDependsOnId
 	 * @return \Combodo\iTop\Form\Form
 	 */
-	public function AddFieldDependency($sFieldId, $sDependsOnId)
-	{
-		if (!array_key_exists($sFieldId, $this->aDependencies))
-		{
-			$this->aDependencies[$sFieldId] = array();
-		}
-		$this->aDependencies[$sFieldId][] = $sDependsOnId;
-		return $this;
-	}
+    public function AddFieldDependency($sFieldId, $sDependsOnId)
+    {
+        if (!array_key_exists($sFieldId, $this->aDependencies))
+        {
+            $this->aDependencies[$sFieldId] = array();
+        }
+        $this->aDependencies[$sFieldId][] = $sDependsOnId;
+        return $this;
+    }
+
+    /**
+     * Returns a hash array of the fields impacts on other fields. Key being the field that impacts the fields stored in the value as a regular array
+     * (It kind of reversed the dependencies array)
+     *
+     * eg :
+     * - 'service' => array('subservice', 'template')
+     * - 'subservice' => array()
+     * - ...
+     *
+     * @return array
+     */
+    public function GetFieldsImpacts()
+    {
+        $aRes = array();
+
+        foreach ($this->aDependencies as $sImpactedFieldId => $aDependentFieldsIds)
+        {
+            foreach ($aDependentFieldsIds as $sDependentFieldId)
+            {
+                if (!array_key_exists($sDependentFieldId, $aRes))
+                {
+                    $aRes[$sDependentFieldId] = array();
+                }
+                $aRes[$sDependentFieldId][] = $sImpactedFieldId;
+            }
+        }
+
+        return $aRes;
+    }
 
 	/**
-	 * Returns a hash array of the fields impacts on other fields. Key being the field that impacts the fields stored in the value as a regular array
-	 * (It kind of reversed the dependencies array)
-	 *
-	 * eg :
-	 * - 'service' => array('subservice', 'template')
-	 * - 'subservice' => array()
-	 * - ...
-	 *
-	 * @return array
+	 * @param $sFormPath
+	 * @return Form|null
 	 */
-	public function GetFieldsImpacts()
+	public function FindSubForm($sFormPath)
 	{
-		$aRes = array();
-
-		foreach ($this->aDependencies as $sImpactedFieldId => $aDependentFieldsIds)
+		$ret = null;
+		if ($sFormPath == $this->sId)
 		{
-			foreach ($aDependentFieldsIds as $sDependentFieldId)
+			$ret = $this;
+		}
+		else
+		{
+			foreach ($this->aFields as $oField)
 			{
-				if (!array_key_exists($sDependentFieldId, $aRes))
+				if ($oField instanceof SubFormField)
 				{
-					$aRes[$sDependentFieldId] = array();
+					$ret = $oField->FindSubForm($sFormPath);
+					if ($ret !== null) break;
 				}
-				$aRes[$sDependentFieldId][] = $sImpactedFieldId;
 			}
 		}
-
-		return $aRes;
+		return $ret;
 	}
 
 	/**
 	 *
 	 */
-	public function Finalize()
-	{
-		//TODO : Call GetOrderedFields
-		// Must call OnFinalize on each fields, regarding the dependencies order
-		// On a SubFormField, will call its own Finalize
-		foreach ($this->aFields as $sId => $oField)
-		{
-			$oField->OnFinalize();
-		}
-	}
+    public function Finalize()
+    {
+        //TODO : Call GetOrderedFields
+        // Must call OnFinalize on each fields, regarding the dependencies order
+        // On a SubFormField, will call its own Finalize
+        foreach ($this->aFields as $sId => $oField)
+        {
+            $oField->OnFinalize();
+        }
+    }
 
 	/**
 	 * Validate the form and return if it's valid or not
 	 * 
 	 * @return boolean
 	 */
-	public function Validate()
-	{
-		$this->SetValid(true);
-		$this->EmptyErrorMessages();
-
-		foreach ($this->aFields as $oField)
-		{
-			if (!$oField->Validate())
-			{
-				$this->SetValid(false);
-				foreach ($oField->GetErrorMessages() as $sErrorMessage)
-				{
-					$this->AddErrorMessage(Dict::S($sErrorMessage), $oField->Getid());
-				}
-			}
-		}
-
-		return $this->GetValid();
-	}
+    public function Validate()
+    {
+        $this->SetValid(true);
+        $this->EmptyErrorMessages();
+
+        foreach ($this->aFields as $oField)
+        {
+            if (!$oField->Validate())
+            {
+                $this->SetValid(false);
+                foreach ($oField->GetErrorMessages() as $sErrorMessage)
+                {
+                    $this->AddErrorMessage(Dict::S($sErrorMessage), $oField->Getid());
+                }
+            }
+        }
+
+        return $this->GetValid();
+    }
 
 }

+ 2 - 0
sources/renderer/console/consoleformrenderer.class.inc.php

@@ -33,7 +33,9 @@ class ConsoleFormRenderer extends FormRenderer
 	public function __construct(Form $oForm)
 	{
 		parent::__construct($oForm);
+		$this->AddSupportedField('HiddenField', 'ConsoleSimpleFieldRenderer');
 		$this->AddSupportedField('StringField', 'ConsoleSimpleFieldRenderer');
+		$this->AddSupportedField('SelectField', 'ConsoleSimpleFieldRenderer');
 		$this->AddSupportedField('SubFormField', 'ConsoleSubFormFieldRenderer');
 	}
 }

+ 73 - 25
sources/renderer/console/fieldrenderer/consolesimplefieldrenderer.class.inc.php

@@ -18,6 +18,7 @@
 
 namespace Combodo\iTop\Renderer\Console\FieldRenderer;
 
+use Combodo\iTop\Form\Field\StringField;
 use \Dict;
 use Combodo\iTop\Renderer\FieldRenderer;
 use Combodo\iTop\Renderer\RenderingOutput;
@@ -29,35 +30,60 @@ class ConsoleSimpleFieldRenderer extends FieldRenderer
 		$oOutput = new RenderingOutput();
 		$sFieldClass = get_class($this->oField);
 
-		// TODO : Shouldn't we have a field type so we don't have to maintain FQN classname ?
-		// Rendering field in edition mode
-		if (!$this->oField->GetReadOnly())
+		if ($sFieldClass == 'Combodo\\iTop\\Form\\Field\\HiddenField')
 		{
+			$oOutput->AddHtml('<input type="hidden" id="'.$this->oField->GetGlobalId().'" value="' . htmlentities($this->oField->GetCurrentValue(), ENT_QUOTES, 'UTF-8') . '"/>');
+		}
+		else
+		{
+			$oOutput->AddHtml('<table class="form-field-container">');
+			$oOutput->AddHtml('<tr>');
+			if ($this->oField->GetLabel() != '')
+			{
+				$oOutput->AddHtml('<td class="form-field-label label"><span><label for="'.$this->oField->GetGlobalId().'">'.$this->oField->GetLabel().'</label></span></td>');
+			}
 			switch ($sFieldClass)
 			{
 				case 'Combodo\\iTop\\Form\\Field\\StringField':
-					if ($this->oField->GetLabel() !== '')
+					$oOutput->AddHtml('<td class="form-field-content">');
+					if ($this->oField->GetReadOnly())
+					{
+						$oOutput->AddHtml('<input type="hidden" id="'.$this->oField->GetGlobalId().'" value="' . htmlentities($this->oField->GetCurrentValue(), ENT_QUOTES, 'UTF-8') . '"/>');
+						$oOutput->AddHtml('<span class="form-field-data">'.htmlentities($this->oField->GetCurrentValue(), ENT_QUOTES, 'UTF-8').'</span>');
+					}
+					else
 					{
-						$oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">' . $this->oField->GetLabel() . '</label>');
+						$oOutput->AddHtml('<input class="form-field-data" type="text" id="'.$this->oField->GetGlobalId().'" value="'.htmlentities($this->oField->GetCurrentValue(), ENT_QUOTES, 'UTF-8').'" size="30"/>');
 					}
-					$oOutput->AddHtml('<input type="text" id="'.$this->oField->GetGlobalId().'" value="' . $this->oField->GetCurrentValue() . '" size="30" />');
+					$oOutput->AddHtml('</td>');
 					$oOutput->AddHtml('<span class="form_validation"></span>');
 					break;
-			}
-		}
-		// ... and in read-only mode
-		else
-		{
-			switch ($sFieldClass)
-			{
-				case 'Combodo\\iTop\\Form\\Field\\StringField':
-					if ($this->oField->GetLabel() !== '')
+
+				case 'Combodo\\iTop\\Form\\Field\\SelectField':
+					$oOutput->AddHtml('<td class="form-field-content">');
+					if ($this->oField->GetReadOnly())
 					{
-						$oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">' . $this->oField->GetLabel() . '</label>');
+						$aChoices = $this->oField->GetChoices();
+						$sCurrentLabel = isset($aChoices[$this->oField->GetCurrentValue()]) ? $aChoices[$this->oField->GetCurrentValue()] : '' ;
+						$oOutput->AddHtml('<input type="hidden" id="'.$this->oField->GetGlobalId().'" value="' . htmlentities($this->oField->GetCurrentValue(), ENT_QUOTES, 'UTF-8') . '"/>');
+						$oOutput->AddHtml('<span class="form-field-data">'.htmlentities($sCurrentLabel, ENT_QUOTES, 'UTF-8').'</span>');
 					}
-					$oOutput->AddHtml('<div class="form-control-static">' . $this->oField->GetCurrentValue() . '</div>');
+					else
+					{
+						$oOutput->AddHtml('<select class="form-field-data" id="'.$this->oField->GetGlobalId().'" '.(($this->oField->GetMultipleValuesEnabled()) ? 'multiple' : '').'>');
+						foreach ($this->oField->GetChoices() as $sChoice => $sLabel)
+						{
+							// Note : The test is a double equal on purpose as the type of the value received from the XHR is not always the same as the type of the allowed values. (eg : string vs int)
+							$sSelectedAtt = ($this->oField->GetCurrentValue() == $sChoice) ? 'selected' : '';
+							$oOutput->AddHtml('<option value="'.htmlentities($sChoice, ENT_QUOTES, 'UTF-8').'" '.$sSelectedAtt.' >'.htmlentities($sLabel, ENT_QUOTES, 'UTF-8').'</option>');
+						}
+						$oOutput->AddHtml('</select>');
+					}
+					$oOutput->AddHtml('</td>');
 					break;
 			}
+			$oOutput->AddHtml('</tr>');
+			$oOutput->AddHtml('</table>');
 		}
 
 		switch ($sFieldClass)
@@ -65,7 +91,22 @@ class ConsoleSimpleFieldRenderer extends FieldRenderer
 			case 'Combodo\\iTop\\Form\\Field\\StringField':
 				$oOutput->AddJs(
 <<<EOF
-                    $("#{$this->oField->GetGlobalId()}").off("change").on("change keyup", function(){
+                    $("#{$this->oField->GetGlobalId()}").off("change keyup").on("change keyup", function(){
+                    	var me = this;
+
+                        $(this).closest(".field_set").trigger("field_change", {
+                            id: $(me).attr("id"),
+                            name: $(me).closest(".form_field").attr("data-field-id"),
+                            value: $(me).val()
+                        });
+                    });
+EOF
+				);
+				break;
+			case 'Combodo\\iTop\\Form\\Field\\SelectField':
+				$oOutput->AddJs(
+<<<EOF
+                    $("#{$this->oField->GetGlobalId()}").off("change").on("change", function(){
                     	var me = this;
 
                         $(this).closest(".field_set").trigger("field_change", {
@@ -88,7 +129,6 @@ EOF
 				'message' => Dict::S($oValidator->GetErrorMessage())
 			);
 		}
-
 		$sValidators = json_encode($aValidators);
 		$sFormFieldOptions =
 <<<EOF
@@ -118,20 +158,28 @@ EOF
 EOF
 			;
 
+		$oOutput->AddJs(
+			<<<EOF
+                    $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").form_field($sFormFieldOptions);
+EOF
+		);
 		switch ($sFieldClass)
 		{
-			case 'Combodo\\iTop\\Form\\Field\\StringField':
-			case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
 			case 'Combodo\\iTop\\Form\\Field\\SelectField':
-			case 'Combodo\\iTop\\Form\\Field\\HiddenField':
-			case 'Combodo\\iTop\\Form\\Field\\RadioField':
-			case 'Combodo\\iTop\\Form\\Field\\CheckboxField':
 				$oOutput->AddJs(
 					<<<EOF
-                    $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").form_field($sFormFieldOptions);
+	                    $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").form_field('option', 'get_current_value_callback', function(me){ return $(me.element).find('select').val();});
 EOF
 				);
 				break;
+
+			case 'Combodo\\iTop\\Form\\Field\\HiddenField':
+			case 'Combodo\\iTop\\Form\\Field\\StringField':
+			case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
+			case 'Combodo\\iTop\\Form\\Field\\HiddenField':
+			case 'Combodo\\iTop\\Form\\Field\\RadioField':
+			case 'Combodo\\iTop\\Form\\Field\\CheckboxField':
+				break;
 		}
 
 		return $oOutput;