Bladeren bron

Rework of the relation diagrams: configuration of the redundancy (AttributeRedundancySettings)

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3552 a333f486-631f-4898-b8df-5754b55c2be0
romainq 10 jaren geleden
bovenliggende
commit
815e3563d3

+ 97 - 16
application/cmdbabstract.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2013 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -247,7 +247,7 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 				$oExtensionInstance->OnDisplayProperties($this, $oPage, $bEditMode);
 			}
 		}
-		
+
 		// Special case to display the case log, if any...
 		// WARNING: if you modify the loop below, also check the corresponding code in UpdateObject and DisplayModifyForm
 		foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
@@ -275,6 +275,8 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 
 	function DisplayBareRelations(WebPage $oPage, $bEditMode = false)
 	{
+		$aRedundancySettings = $this->FindVisibleRedundancySettings();
+
 		// Related objects: display all the linkset attributes, each as a separate tab
 		// In the order described by the 'display' ZList
 		$aList = $this->FlattenZList(MetaModel::GetZListItems(get_class($this), 'details'));
@@ -340,6 +342,7 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 			// Non-readable/hidden linkedset... don't display anything
 			if ($iFlags & OPT_ATT_HIDDEN) continue;
 			
+			$aArgs = array('this' => $this);
 			$bReadOnly = ($iFlags & (OPT_ATT_READONLY|OPT_ATT_SLAVE));
 			if ($bEditMode && (!$bReadOnly))
 			{
@@ -359,7 +362,6 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 
 				$oValue = $this->Get($sAttCode);
 				$sDisplayValue = ''; // not used
-				$aArgs = array('this' => $this);
 				$sHTMLValue = "<span id=\"field_{$sInputId}\">".self::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $oValue, $sDisplayValue, $sInputId, '', $iFlags, $aArgs).'</span>';
 				$this->AddToFieldsMap($sAttCode,  $sInputId);
 				$oPage->add($sHTMLValue);
@@ -411,6 +413,29 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 				$oBlock = new DisplayBlock($this->Get($sAttCode)->GetFilter(), 'list', false);
 				$oBlock->Display($oPage, 'rel_'.$sAttCode, $aParams);
 			}
+			if (array_key_exists($sAttCode, $aRedundancySettings))
+			{
+				foreach ($aRedundancySettings[$sAttCode] as $oRedundancyAttDef)
+				{
+					$sRedundancyAttCode = $oRedundancyAttDef->GetCode();
+					$sValue = $this->Get($sRedundancyAttCode);
+					$iRedundancyFlags = $this->GetFormAttributeFlags($sRedundancyAttCode);
+					$bRedundancyReadOnly = ($iRedundancyFlags & (OPT_ATT_READONLY|OPT_ATT_SLAVE));
+
+					$oPage->add('<fieldset>');
+					$oPage->add('<legend>'.$oRedundancyAttDef->GetLabel().'</legend>');
+					if ($bEditMode && (!$bRedundancyReadOnly))
+					{
+						$sInputId = $this->m_iFormId.'_'.$sRedundancyAttCode;
+						$oPage->add("<span id=\"field_{$sInputId}\">".self::GetFormElementForField($oPage, $sClass, $sRedundancyAttCode, $oRedundancyAttDef, $sValue, '', $sInputId, '', $iFlags, $aArgs).'</span>');
+					}
+					else
+					{
+						$oPage->add($oRedundancyAttDef->GetDisplayForm($sValue, $oPage, false, $this->m_iFormId));
+					}
+					$oPage->add('</fieldset>');
+				}
+			}
 		}
 		$oPage->SetCurrentTab('');
 
@@ -527,18 +552,7 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 
 						$sComments = isset($aFieldsComments[$sAttCode]) ? $aFieldsComments[$sAttCode] : '&nbsp;';
 						$sInfos = '&nbsp;';
-						if ($this->IsNew())
-						{
-							$iFlags = $this->GetInitialStateAttributeFlags($sAttCode);
-						}
-						else
-						{
-							$iFlags = $this->GetAttributeFlags($sAttCode);
-						}
-						if (($iFlags & OPT_ATT_MANDATORY) && $this->IsNew())
-						{
-							$iFlags = $iFlags & ~OPT_ATT_READONLY; // Mandatory fields cannot be read-only when creating an object
-						}
+						$iFlags = $this->GetFormAttributeFlags($sAttCode);
 						if (array_key_exists($sAttCode, $aExtraFlags))
 						{
 							// the caller may override some flags if needed
@@ -1796,6 +1810,20 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 					$sHTMLValue .= "<!-- iFlags: $iFlags bMandatory: $bMandatory -->\n";
 					break;
 					
+				case 'RedundancySetting':
+					$sHTMLValue = '<table>';
+					$sHTMLValue .= '<tr>';
+					$sHTMLValue .= '<td>';
+					$sHTMLValue .= '<div id="'.$iId.'">';
+					$sHTMLValue .= $oAttDef->GetDisplayForm($value, $oPage, true);
+					$sHTMLValue .= '</div>';
+					$sHTMLValue .= '</td>';
+					$sHTMLValue .= '<td>'.$sValidationField.'</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 'String':
 				default:
 					$aEventsList[] ='validate';
@@ -2633,6 +2661,26 @@ EOF
 	}
 
 	/**
+	 * Compute the attribute flags depending on the object state
+	 */	
+	public function GetFormAttributeFlags($sAttCode)
+	{
+		if ($this->IsNew())
+		{
+			$iFlags = $this->GetInitialStateAttributeFlags($sAttCode);
+		}
+		else
+		{
+			$iFlags = $this->GetAttributeFlags($sAttCode);
+		}
+		if (($iFlags & OPT_ATT_MANDATORY) && $this->IsNew())
+		{
+			$iFlags = $iFlags & ~OPT_ATT_READONLY; // Mandatory fields cannot be read-only when creating an object
+		}
+		return $iFlags;
+	}
+
+	/**
 	 * Updates the object from a flat array of values
 	 * @param string $aValues array of attcode => scalar or array (N-N links)
 	 * @return void
@@ -2826,6 +2874,10 @@ EOF
 			{
 				$value = array('fcontents' => utils::ReadPostedDocument("attr_{$sFormPrefix}{$sAttCode}", 'fcontents'));
 			}
+			elseif ($oAttDef->GetEditClass() == 'RedundancySetting')
+			{
+				$value = $oAttDef->ReadValueFromPostedForm($sFormPrefix);
+			}
 			else if (($oAttDef->GetEditClass() == 'LinkedSet') && !$oAttDef->IsIndirect() &&
 			         (($oAttDef->GetEditMode() == LINKSET_EDITMODE_INPLACE) || ($oAttDef->GetEditMode() == LINKSET_EDITMODE_ADDREMOVE)) )
 			{
@@ -3748,5 +3800,34 @@ EOF
 			}
 		}
 	}
+
+	/**
+	 * Find redundancy settings that can be viewed and modified in a tab
+	 * Settings are distributed to the corresponding link set attribute so as to be shown in the relevant tab	 
+	 */	 	
+	protected function FindVisibleRedundancySettings()
+	{
+		$aRet = array();
+		foreach (MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
+		{
+			if ($oAttDef instanceof AttributeRedundancySettings)
+			{
+				if ($oAttDef->IsVisible())
+				{
+					$aQueryInfo = $oAttDef->GetRelationQueryData();
+					if (isset($aQueryInfo['sAttribute']))
+					{
+						$oUpperAttDef = MetaModel::GetAttributeDef($aQueryInfo['sFromClass'], $aQueryInfo['sAttribute']);
+						$oHostAttDef = $oUpperAttDef->GetMirrorLinkAttribute();
+						if ($oHostAttDef)
+						{
+							$sHostAttCode = $oHostAttDef->GetCode();
+							$aRet[$sHostAttCode][] = $oAttDef;
+						}
+					}
+				}
+			}
+		}	
+		return $aRet;
+	}
 }
-?>

+ 452 - 1
core/attributedef.class.inc.php

@@ -215,6 +215,7 @@ abstract class AttributeDefinition
 	public function GetValue($oHostObject){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;}
 
 	/**
 	 * Helper to browse the hierarchy of classes, searching for a label
@@ -979,6 +980,17 @@ class AttributeLinkedSet extends AttributeDefinition
 		// Both values are Object sets
 		return $val1->HasSameContents($val2);
 	}
+
+	/**
+	 * Find the corresponding "link" attribute on the target class
+	 * 	 
+	 * @return string The attribute code on the target class, or null if none has been found
+	 */
+	public function GetMirrorLinkAttribute()
+	{
+		$oRemoteAtt = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToMe());
+		return $oRemoteAtt;
+	}
 }
 
 /**
@@ -1001,6 +1013,28 @@ class AttributeLinkedSetIndirect extends AttributeLinkedSet
 	{
 		return $this->GetOptional('tracking_level', MetaModel::GetConfig()->Get('tracking_level_linked_set_indirect_default'));
 	}
+
+	/**
+	 * Find the corresponding "link" attribute on the target class
+	 * 	 
+	 * @return string The attribute code on the target class, or null if none has been found
+	 */
+	public function GetMirrorLinkAttribute()
+	{
+		$oRet = null;
+		$oExtKeyToRemote = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote());
+		$sRemoteClass = $oExtKeyToRemote->GetTargetClass();
+		foreach (MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef)
+		{
+			if (!$oRemoteAttDef instanceof AttributeLinkedSetIndirect) continue;
+			if ($oRemoteAttDef->GetLinkedClass() != $this->GetLinkedClass()) continue;
+			if ($oRemoteAttDef->GetExtKeyToMe() != $this->GetExtKeyToRemote()) continue;
+			if ($oRemoteAttDef->GetExtKeyToRemote() != $this->GetExtKeyToMe()) continue;
+			$oRet = $oRemoteAttDef;
+			break;
+		}
+		return $oRet;
+	}
 }
 
 /**
@@ -3171,6 +3205,26 @@ class AttributeExternalKey extends AttributeDBFieldVoid
 	{
 		return $this->GetOptional('allow_target_creation', MetaModel::GetConfig()->Get('allow_target_creation'));
 	}
+
+	/**
+	 * Find the corresponding "link" attribute on the target class
+	 * 	 
+	 * @return string The attribute code on the target class, or null if none has been found
+	 */
+	public function GetMirrorLinkAttribute()
+	{
+		$oRet = null;
+		$sRemoteClass = $this->GetTargetClass();
+		foreach (MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef)
+		{
+			if (!$oRemoteAttDef->IsLinkSet()) continue;
+			if (!is_subclass_of($this->GetHostClass(), $oRemoteAttDef->GetLinkedClass()) && $oRemoteAttDef->GetLinkedClass() != $this->GetHostClass()) continue;
+			if ($oRemoteAttDef->GetExtKeyToMe() != $this->GetCode()) continue;
+			$oRet = $oRemoteAttDef;
+			break;
+		}
+		return $oRet;
+	}
 }
 
 /**
@@ -3295,6 +3349,16 @@ class AttributeHierarchicalKey extends AttributeExternalKey
 		$oSet = $oValSetDef->ToObjectSet($aArgs, $sContains);
 		return $oSet;
 	}
+
+	/**
+	 * Find the corresponding "link" attribute on the target class
+	 * 	 
+	 * @return string The attribute code on the target class, or null if none has been found
+	 */
+	public function GetMirrorLinkAttribute()
+	{
+		return null;
+	}
 }
 
 /**
@@ -5107,4 +5171,391 @@ class AttributeFriendlyName extends AttributeComputedFieldVoid
 	} 
 }
 
-?>
+/**
+ * Holds the setting for the redundancy on a specific relation
+ * Its value is a string, containing either:
+ * - 'disabled'
+ * - 'n', where n is a positive integer value giving the minimum count of items upstream
+ * - 'n%', where n is a positive integer value, giving the minimum as a percentage of the total count of items upstream
+ *
+ * @package     iTopORM
+ */
+class AttributeRedundancySettings extends AttributeDBField
+{
+	static public function ListExpectedParams()
+	{
+		return array('sql', 'relation_code', 'from_class', 'neighbour_id', 'enabled', 'enabled_mode', 'min_up', 'min_up_type', 'min_up_mode');
+	}
+
+	public function GetValuesDef() {return null;} 
+	public function GetPrerequisiteAttributes() {return array();} 
+
+	public function GetEditClass() {return "RedundancySetting";}
+	protected function GetSQLCol() {return "VARCHAR(20)";}
+
+
+	public function GetValidationPattern()
+	{
+		return "^[0-9]{1,3}|[0-9]{1,2}%|disabled$";
+	}
+
+	public function GetMaxSize()
+	{
+		return 20;
+	}
+
+	public function GetDefaultValue($aArgs = array())
+	{
+		$sRet = 'disabled';
+		if ($this->Get('enabled'))
+		{
+			if ($this->Get('min_up_type') == 'count')
+			{
+				$sRet = (string) $this->Get('min_up');
+			}
+			else // percent
+			{
+				$sRet = $this->Get('min_up').'%';
+			}
+		}
+		return $sRet;
+	}
+
+	public function IsNullAllowed()
+	{
+		return false;
+	} 
+
+	public function GetNullValue()
+	{
+		return '';
+	} 
+
+	public function IsNull($proposedValue)
+	{
+		return ($proposedValue == '');
+	} 
+
+	public function MakeRealValue($proposedValue, $oHostObj)
+	{
+		if (is_null($proposedValue)) return '';
+		return (string)$proposedValue;
+	}
+
+	public function ScalarToSQL($value)
+	{
+		if (!is_string($value))
+		{
+			throw new CoreException('Expected the attribute value to be a string', array('found_type' => gettype($value), 'value' => $value, 'class' => $this->GetHostClass(), 'attribute' => $this->GetCode()));
+		}
+		return $value;
+	}
+
+	public function GetRelationQueryData()
+	{
+		foreach (MetaModel::EnumRelationQueries($this->GetHostClass(), $this->Get('relation_code'), false) as $sDummy => $aQueryInfo)
+		{
+			if ($aQueryInfo['sFromClass'] == $this->Get('from_class'))
+			{
+				if ($aQueryInfo['sNeighbour'] == $this->Get('neighbour_id'))
+				{
+					return $aQueryInfo;
+				}
+			}
+		}
+	}
+
+	/**
+	 * Find the user option label
+	 * @param user option : disabled|cout|percent	 	
+	 */	
+	public function GetUserOptionFormat($sUserOption, $sDefault = null)
+	{
+		$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/'.$sUserOption, null, true /*user lang*/);
+		if (is_null($sLabel))
+		{
+			// If no default value is specified, let's define the most relevant one for developping purposes
+			if (is_null($sDefault))
+			{
+				$sDefault = str_replace('_', ' ', $this->m_sCode.':'.$sUserOption.'(%1$s)');
+			}
+			// Browse the hierarchy again, accepting default (english) translations
+			$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/'.$sUserOption, $sDefault, false);
+		}
+		return $sLabel;
+	}
+
+	/**
+	 * Override to display the value in the GUI
+	 */	
+	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
+	{
+		$sCurrentOption = $this->GetCurrentOption($sValue);
+		$sClass = $oHostObject ? get_class($oHostObject) : $this->m_sHostClass;
+		return sprintf($this->GetUserOptionFormat($sCurrentOption), $this->GetMinUpValue($sValue), MetaModel::GetName($sClass));
+	}
+
+	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true)
+	{
+		$sFrom = array("\r\n", $sTextQualifier);
+		$sTo = array("\n", $sTextQualifier.$sTextQualifier);
+		$sEscaped = str_replace($sFrom, $sTo, (string)$sValue);
+		return $sTextQualifier.$sEscaped.$sTextQualifier;
+	}
+
+	/**
+	 * Helper to interpret the value, given the current settings and string representation of the attribute	
+	 */	
+	public function IsEnabled($sValue)
+	{
+		if ($this->get('enabled_mode') == 'fixed')
+		{
+			$bRet = $this->get('enabled');
+		}
+		else
+		{
+			$bRet = ($sValue != 'disabled');
+		}
+		return $bRet;
+	}
+
+	/**
+	 * Helper to interpret the value, given the current settings and string representation of the attribute	
+	 */	
+	public function GetMinUpType($sValue)
+	{
+		if ($this->get('min_up_mode') == 'fixed')
+		{
+			$sRet = $this->get('min_up_type');
+		}
+		else
+		{
+			$sRet = 'count';
+			if (substr(trim($sValue), -1, 1) == '%')
+			{
+				$sRet = 'percent';
+			}
+		}
+		return $sRet;
+	}
+
+	/**
+	 * Helper to interpret the value, given the current settings and string representation of the attribute	
+	 */	
+	public function GetMinUpValue($sValue)
+	{
+		if ($this->get('min_up_mode') == 'fixed')
+		{
+			$iRet = (int) $this->Get('min_up');
+		}
+		else
+		{
+			$sRefValue = $sValue;
+			if (substr(trim($sValue), -1, 1) == '%')
+			{
+				$sRefValue = substr(trim($sValue), 0, -1);
+			}
+			$iRet = (int) trim($sRefValue);
+		}
+		return $iRet;
+	}
+
+	/**
+	 * Helper to determine if the redundancy can be viewed/edited by the end-user
+	 */	
+	public function IsVisible()
+	{
+		$bRet = false;
+		if ($this->Get('enabled_mode') == 'fixed')
+		{
+			$bRet = $this->Get('enabled');
+		}
+		elseif ($this->Get('enabled_mode') == 'user')
+		{
+			$bRet = true;
+		}
+		return $bRet;
+	}
+
+	public function IsWritable()
+	{
+		if (($this->Get('enabled_mode') == 'fixed') && ($this->Get('min_up_mode') == 'fixed'))
+		{
+			return false;
+		}
+		return true;
+	}
+
+	/**
+	 * Returns an HTML form that can be read by ReadValueFromPostedForm
+	 */	
+	public function GetDisplayForm($sCurrentValue, $oPage, $bEditMode = false, $sFormPrefix = '')
+	{
+		$sRet = '';
+		$aUserOptions = $this->GetUserOptions($sCurrentValue);
+		if (count($aUserOptions) < 2)
+		{
+			$bEditOption = false;
+		}
+		else
+		{
+			$bEditOption = $bEditMode;
+		}
+		$sCurrentOption = $this->GetCurrentOption($sCurrentValue);
+		foreach($aUserOptions as $sUserOption)
+		{
+			$bSelected = ($sUserOption == $sCurrentOption);
+			$sRet .= '<div>';
+			$sRet .= $this->GetDisplayOption($sCurrentValue, $oPage, $sFormPrefix, $bEditOption, $sUserOption, $bSelected);
+			$sRet .= '</div>';
+		}
+		return $sRet;
+	}
+
+	const USER_OPTION_DISABLED = 'disabled';
+	const USER_OPTION_ENABLED_COUNT = 'count';
+	const USER_OPTION_ENABLED_PERCENT = 'percent';
+
+	/**
+	 * Depending on the xxx_mode parameters, build the list of options that are allowed to the end-user
+	 */	 	
+	protected function GetUserOptions($sValue)
+	{
+		$aRet = array();
+		if ($this->Get('enabled_mode') == 'user')
+		{
+			$aRet[] = self::USER_OPTION_DISABLED;
+		}
+		
+		if ($this->Get('min_up_mode') == 'user')
+		{
+			$aRet[] = self::USER_OPTION_ENABLED_COUNT;
+			$aRet[] = self::USER_OPTION_ENABLED_PERCENT;
+		}
+		else
+		{
+			if ($this->GetMinUpType($sValue) == 'count')
+			{
+				$aRet[] = self::USER_OPTION_ENABLED_COUNT;
+			}
+			else
+			{
+				$aRet[] = self::USER_OPTION_ENABLED_PERCENT;
+			}
+		}
+		return $aRet;
+	}
+
+	/**
+	 * Convert the string representation into one of the existing options	
+	 */	
+	protected function GetCurrentOption($sValue)
+	{
+		$sRet = self::USER_OPTION_DISABLED;
+		if ($this->IsEnabled($sValue))
+		{
+			if ($this->GetMinUpType($sValue) == 'count')
+			{
+				$sRet = self::USER_OPTION_ENABLED_COUNT;
+			}
+			else
+			{
+				$sRet = self::USER_OPTION_ENABLED_PERCENT;
+			}
+		}
+		return $sRet;
+	}
+
+	/**
+	 * Display an option (form, or current value)
+	 */	 	
+	protected function GetDisplayOption($sCurrentValue, $oPage, $sFormPrefix, $bEditMode, $sUserOption, $bSelected = true)
+	{
+		$sRet = '';
+
+		$iCurrentValue = $this->GetMinUpValue($sCurrentValue);
+		if ($bEditMode)
+		{
+			$sHtmlNamesPrefix = 'rddcy_'.$this->Get('relation_code').'_'.$this->Get('from_class').'_'.$this->Get('neighbour_id');
+			switch ($sUserOption)
+			{
+			case self::USER_OPTION_DISABLED:
+				$sValue = ''; // Empty placeholder
+				break;
+	
+			case self::USER_OPTION_ENABLED_COUNT:
+				if ($bEditMode)
+				{
+					$sName = $sHtmlNamesPrefix.'_min_up_count';
+					$sEditValue = $bSelected ? $iCurrentValue : '';
+					$sValue = '<input class="redundancy-min-up-count" type="string" size="3" name="'.$sName.'" value="'.$sEditValue.'">';
+					// To fix an issue on Firefox: focus set to the option (because the input is within the label for the option)
+					$oPage->add_ready_script("\$('[name=\"$sName\"]').click(function(){var me=this; setTimeout(function(){\$(me).focus();}, 100);});");
+				}
+				else
+				{
+					$sValue = $iCurrentValue;
+				}
+				break;
+	
+			case self::USER_OPTION_ENABLED_PERCENT:
+				if ($bEditMode)
+				{
+					$sName = $sHtmlNamesPrefix.'_min_up_percent';
+					$sEditValue = $bSelected ? $iCurrentValue : '';
+					$sValue = '<input class="redundancy-min-up-percent" type="string" size="3" name="'.$sName.'" value="'.$sEditValue.'">';
+					// To fix an issue on Firefox: focus set to the option (because the input is within the label for the option)
+					$oPage->add_ready_script("\$('[name=\"$sName\"]').click(function(){var me=this; setTimeout(function(){\$(me).focus();}, 100);});");
+				}
+				else
+				{
+					$sValue = $iCurrentValue;
+				}
+				break;
+			}
+			$sLabel = sprintf($this->GetUserOptionFormat($sUserOption), $sValue, MetaModel::GetName($this->GetHostClass()));
+
+			$sOptionName = $sHtmlNamesPrefix.'_user_option';
+			$sOptionId = $sOptionName.'_'.$sUserOption;
+			$sChecked = $bSelected ? 'checked' : '';
+			$sRet = '<input type="radio" name="'.$sOptionName.'" id="'.$sOptionId.'" value="'.$sUserOption.'"'.$sChecked.'> <label for="'.$sOptionId.'">'.$sLabel.'</label>';
+		}
+		else
+		{
+			// Read-only: display only the currently selected option
+			if ($bSelected)
+			{
+				$sRet = sprintf($this->GetUserOptionFormat($sUserOption), $iCurrentValue, MetaModel::GetName($this->GetHostClass()));
+			}
+		}
+		return $sRet;
+	}
+
+	/**
+	 * Makes the string representation out of the values given by the form defined in GetDisplayForm	
+	 */	
+	public function ReadValueFromPostedForm($sFormPrefix)
+	{
+		$sHtmlNamesPrefix = 'rddcy_'.$this->Get('relation_code').'_'.$this->Get('from_class').'_'.$this->Get('neighbour_id');
+
+		$iMinUpCount = (int) utils::ReadPostedParam($sHtmlNamesPrefix.'_min_up_count', null, 'raw_data');
+		$iMinUpPercent = (int) utils::ReadPostedParam($sHtmlNamesPrefix.'_min_up_percent', null, 'raw_data');
+		$sSelectedOption = utils::ReadPostedParam($sHtmlNamesPrefix.'_user_option', null, 'raw_data');
+		switch ($sSelectedOption)
+		{
+		case self::USER_OPTION_ENABLED_COUNT:
+			$sRet = $iMinUpCount;
+			break;
+
+		case self::USER_OPTION_ENABLED_PERCENT:
+			$sRet = $iMinUpPercent.'%';
+			break;
+
+		case self::USER_OPTION_DISABLED:
+		default:
+			$sRet = 'disabled';
+			break;
+		}
+		return $sRet;
+	}
+}

+ 0 - 1
core/cmdbobject.class.inc.php

@@ -63,7 +63,6 @@ require_once('dbobjectset.class.php');
 require_once('backgroundprocess.inc.php');
 require_once('asynctask.class.inc.php');
 require_once('dbproperty.class.inc.php');
-require_once('redundancysettings.class.inc.php');
 
 // db change tracking data model
 require_once('cmdbchange.class.inc.php');

+ 11 - 1
core/metamodel.class.php

@@ -4402,7 +4402,17 @@ abstract class MetaModel
 					$aTableInfo['Fields'][$sField]['used'] = true;
 
 					$bIndexNeeded = $oAttDef->RequiresIndex();
-					$sFieldDefinition = "`$sField` ".($oAttDef->IsNullAllowed() ? "$sDBFieldType NULL" : "$sDBFieldType NOT NULL");
+					// Note: This fix deals only with the case when the field is MISSING
+					// it won't deal with the case when the field gets modified
+					if ($oAttDef->IsNullAllowed())
+					{
+						$sFieldDefinition = "`$sField` $sDBFieldType NULL";
+					}
+					else
+					{
+						$aDefaults = $oAttDef->GetSQLValues($oAttDef->GetDefaultValue());
+						$sFieldDefinition = "`$sField` $sDBFieldType NOT NULL DEFAULT ".CMDBSource::Quote($aDefaults[$sField]);
+					}
 					if (!CMDBSource::IsField($sTable, $sField))
 					{
 						$aErrors[$sClass][$sAttCode][] = "field '$sField' could not be found in table '$sTable'";

+ 0 - 98
core/redundancysettings.class.inc.php

@@ -1,98 +0,0 @@
-<?php
-// Copyright (C) 2015 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/>
-
-/**
- * Persistent classes (internal): user settings for the redundancy
- *
- * @copyright   Copyright (C) 2015 Combodo SARL
- * @license     http://opensource.org/licenses/AGPL-3.0
- */
-
-
-/**
- * Redundancy settings
- *
- * @package     iTopORM
- */
-class RedundancySettings extends DBObject
-{
-	public static function Init()
-	{
-		$aParams = array
-		(
-			"category" => "core/cmdb",
-			"key_type" => "autoincrement",
-			"name_attcode" => array('relation_code','from_class','neighbour','objkey'),
-			"state_attcode" => "",
-			"reconc_keys" => array(),
-			"db_table" => "priv_redundancy_settings",
-			"db_key_field" => "id",
-			"db_finalclass_field" => "finalclass",
-			"display_template" => "",
-			'indexes' => array(
-				array('relation_code', 'from_class', 'neighbour', 'objclass', 'objkey'),
-			)
-		);
-		MetaModel::Init_Params($aParams);
-		MetaModel::Init_AddAttribute(new AttributeString("relation_code", array("allowed_values"=>null, "sql"=>"relation_code", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
-		MetaModel::Init_AddAttribute(new AttributeString("from_class", array("allowed_values"=>null, "sql"=>"from_class", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
-		MetaModel::Init_AddAttribute(new AttributeString("neighbour", array("allowed_values"=>null, "sql"=>"neighbour", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
-
-		MetaModel::Init_AddAttribute(new AttributeString("objclass", array("allowed_values"=>null, "sql"=>"objclass", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
-		MetaModel::Init_AddAttribute(new AttributeObjectKey("objkey", array("allowed_values"=>null, "class_attcode"=>"objclass", "sql"=>"objkey", "is_null_allowed"=>false, "depends_on"=>array())));
-
-		MetaModel::Init_AddAttribute(new AttributeBoolean("enabled", array("allowed_values"=>null, "sql"=>"enabled", "default_value"=>false, "is_null_allowed"=>false, "depends_on"=>array())));
-
-		MetaModel::Init_AddAttribute(new AttributeEnum("min_up_type", array("allowed_values"=>new ValueSetEnum('count,percent'), "sql"=>"min_up_type", "default_value"=>"count", "is_null_allowed"=>false, "depends_on"=>array())));
-		MetaModel::Init_AddAttribute(new AttributeInteger("min_up_count", array("allowed_values"=>null, "sql"=>"min_up_count", "default_value"=>1, "is_null_allowed"=>true, "depends_on"=>array())));
-		MetaModel::Init_AddAttribute(new AttributeInteger("min_up_percent", array("allowed_values"=>null, "sql"=>"min_up_percent", "default_value"=>50, "is_null_allowed"=>true, "depends_on"=>array())));
-	}
-
-	public static function MakeDefault($sRelCode, $aQueryInfo, $oToObject)
-	{
-		$oRet = MetaModel::NewObject('RedundancySettings');
-		$oRet->Set('relation_code', $sRelCode);
-		$oRet->Set('from_class', $aQueryInfo['sFromClass']);
-		$oRet->Set('neighbour', $aQueryInfo['sNeighbour']);
-		$oRet->Set('objclass', get_class($oToObject));
-		$oRet->Set('objkey', $oToObject->GetKey());
-		$oRet->Set('enabled', $aQueryInfo['bRedundancyEnabledValue']);
-		$oRet->Set('min_up_type', $aQueryInfo['sRedundancyMinUpType']);
-		$oRet->Set('min_up_count', ($aQueryInfo['sRedundancyMinUpType'] == 'count') ? $aQueryInfo['iRedundancyMinUpValue'] : 1);
-		$oRet->Set('min_up_percent', ($aQueryInfo['sRedundancyMinUpType'] == 'percent') ? $aQueryInfo['iRedundancyMinUpValue'] : 50);
-		return $oRet;
-	}
-
-	public static function GetSettings($sRelCode, $aQueryInfo, $oToObject)
-	{
-		$oSearch = new DBObjectSearch('RedundancySettings');
-		$oSearch->AddCondition('relation_code', $sRelCode, '=');
-		$oSearch->AddCondition('from_class', $aQueryInfo['sFromClass'], '=');
-		$oSearch->AddCondition('neighbour', $aQueryInfo['sNeighbour'], '=');
-		$oSearch->AddCondition('objclass', get_class($oToObject), '=');
-		$oSearch->AddCondition('objkey', $oToObject->GetKey(), '=');
-
-		$oSet = new DBObjectSet($oSearch);
-		$oRet = $oSet->Fetch();
-		if (!$oRet)
-		{
-			$oRet = self::MakeDefault($sRelCode, $aQueryInfo, $oToObject);
-		}
-		return $oRet;
-	}
-}

+ 30 - 43
core/relationgraph.class.inc.php

@@ -382,17 +382,12 @@ class RelationGraph extends SimpleGraph
 	protected function IsRedundancyEnabled($sRelCode, $aQueryInfo, $oToNode)
 	{
 		$bRet = false;
-		if (isset($aQueryInfo['sRedundancyEnabledMode']))
+		$oToObject = $oToNode->GetProperty('object');
+		$oRedundancyAttDef = $this->FindRedundancyAttribute($sRelCode, $aQueryInfo, get_class($oToObject));
+		if ($oRedundancyAttDef)
 		{
-			if ($aQueryInfo['sRedundancyEnabledMode'] == 'fixed')
-			{
-				$bRet = $aQueryInfo['bRedundancyEnabledValue'];
-			}
-			elseif ($aQueryInfo['sRedundancyEnabledMode'] == 'user')
-			{
-				$oUserSettings = $this->FindRedundancyUserSettings($sRelCode, $aQueryInfo, $oToNode);
-				$bRet = $oUserSettings->Get('enabled');
-			}
+			$sValue = $oToObject->Get($oRedundancyAttDef->GetCode());
+			$bRet = $oRedundancyAttDef->IsEnabled($sValue);
 		}
 		return $bRet;
 	}
@@ -403,52 +398,44 @@ class RelationGraph extends SimpleGraph
 	protected function GetRedundancyMinUp($sRelCode, $aQueryInfo, $oToNode, $iUpstreamObjects)
 	{
 		$iMinUp = 0;
-		if (isset($aQueryInfo['sRedundancyMinUpMode']))
+
+		$oToObject = $oToNode->GetProperty('object');
+		$oRedundancyAttDef = $this->FindRedundancyAttribute($sRelCode, $aQueryInfo, get_class($oToObject));
+		if ($oRedundancyAttDef)
 		{
-			if ($aQueryInfo['sRedundancyMinUpMode'] == 'fixed')
+			$sValue = $oToObject->Get($oRedundancyAttDef->GetCode());
+			if ($oRedundancyAttDef->GetMinUpType($sValue) == 'count')
 			{
-				if ($aQueryInfo['sRedundancyMinUpType'] == 'count')
-				{
-					$iMinUp = $aQueryInfo['iRedundancyMinUpValue'];
-				}
-				else // 'percent' assumed
-				{
-					$iMinUp = $iUpstreamObjects * $aQueryInfo['iRedundancyMinUpValue'] / 100;
-				}
+				$iMinUp = $oRedundancyAttDef->GetMinUpValue($sValue);
 			}
-			elseif ($aQueryInfo['sRedundancyMinUpMode'] == 'user')
+			else
 			{
-				$oUserSettings = $this->FindRedundancyUserSettings($sRelCode, $aQueryInfo, $oToNode);
-				if ($oUserSettings->Get('min_up_type') == 'count')
-				{
-					$iMinUp = $oUserSettings->Get('min_up_count');
-				}
-				else
-				{
-					$iMinUp = $iUpstreamObjects * $oUserSettings->Get('min_up_percent') / 100;
-				}
+				$iMinUp = $iUpstreamObjects * $oRedundancyAttDef->GetMinUpValue($sValue) / 100;
 			}
 		}
 		return $iMinUp;
 	}
 
 	/**
-	 * Helper to search for and cache the reduncancy user settings (could be an object NOT recorded in the DB)	
+	 * Helper to search for the redundancy attribute	
 	 */	
-	protected function FindRedundancyUserSettings($sRelCode, $aQueryInfo, $oToNode)
+	protected function FindRedundancyAttribute($sRelCode, $aQueryInfo, $sClass)
 	{
-		$sNeighbourKey = $sRelCode.'/'.$aQueryInfo['sFromClass'].'/'.$aQueryInfo['sNeighbour'];
-		if (isset($this->aRedundancySettings[$sNeighbourKey][$oToNode->GetId()]))
-		{
-			// Cache hit
-			$oUserSettings = $this->aRedundancySettings[$sNeighbourKey][$oToNode->GetId()];
-		}
-		else
+		$oRet = null;
+		foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
 		{
-			// Cache miss: build the entry
-			$oUserSettings = RedundancySettings::GetSettings($sRelCode, $aQueryInfo, $oToNode->GetProperty('object'));
-			$this->aRedundancySettings[$sNeighbourKey][$oToNode->GetId()] = $oUserSettings;
+			if ($oAttDef instanceof AttributeRedundancySettings)
+			{
+				if ($oAttDef->Get('relation_code') == $sRelCode)
+				{
+					if ($oAttDef->Get('neighbour_id') == $aQueryInfo['sNeighbour'])
+					{
+						$oRet = $oAttDef;
+						break;
+					}
+				}
+			}
 		}
-		return $oUserSettings;
+		return $oRet;
 	}
 }

+ 42 - 15
datamodels/2.x/itop-config-mgmt/datamodel.itop-config-mgmt.xml

@@ -1472,17 +1472,6 @@
             </neighbour>
             <neighbour id="applicationsolution">
               <attribute>applicationsolution_list</attribute>
-              <redundancy>
-                <enabled>
-                  <value>false</value>
-                  <mode>user</mode>
-                </enabled>
-                <min_up>
-                  <type>count</type>
-                  <value>1</value>
-                  <mode>user</mode>
-                </min_up>
-              </redundancy>
             </neighbour>
             <neighbour id="softwareinstance">
               <attribute>softwares_list</attribute>
@@ -2000,6 +1989,17 @@
           <ext_key_to_remote>san_id</ext_key_to_remote>
           <duplicates/>
         </field>
+        <field id="redundancy" xsi:type="AttributeRedundancySettings">
+          <sql>redundancy</sql>
+          <relation_code>impacts</relation_code>
+          <from_class>PowerConnection</from_class>
+          <neighbour_id>datacenterdevice</neighbour_id>
+          <enabled>true</enabled>
+          <enabled_mode>fixed</enabled_mode>
+          <min_up>1</min_up>
+          <min_up_type>count</min_up_type>
+          <min_up_mode>fixed</min_up_mode>
+        </field>
       </fields>
       <methods/>
       <presentation>
@@ -2316,7 +2316,7 @@
                     </item>
                   </items>
                 </item>
-                <item id="fieldset:Server:otherinfo">
+                <item id="fieldset:Server:power">
                   <rank>20</rank>
                   <items>
                     <item id="powerA_id">
@@ -2325,11 +2325,19 @@
                     <item id="powerB_id">
                       <rank>20</rank>
                     </item>
-                    <item id="description">
+                    <item id="redundancy">
                       <rank>30</rank>
                     </item>
                   </items>
                 </item>
+                <item id="fieldset:Server:otherinfo">
+                  <rank>30</rank>
+                  <items>
+                    <item id="description">
+                      <rank>10</rank>
+                    </item>
+                  </items>
+                </item>
               </items>
             </item>
           </items>
@@ -2640,7 +2648,7 @@
                     </item>
                   </items>
                 </item>
-                <item id="fieldset:Server:otherinfo">
+                <item id="fieldset:Server:power">
                   <rank>20</rank>
                   <items>
                     <item id="powerA_id">
@@ -2649,11 +2657,19 @@
                     <item id="powerB_id">
                       <rank>20</rank>
                     </item>
-                    <item id="description">
+                    <item id="redundancy">
                       <rank>30</rank>
                     </item>
                   </items>
                 </item>
+                <item id="fieldset:Server:otherinfo">
+                  <rank>30</rank>
+                  <items>
+                    <item id="description">
+                      <rank>10</rank>
+                    </item>
+                  </items>
+                </item>
               </items>
             </item>
           </items>
@@ -2807,6 +2823,17 @@
           <is_null_allowed>true</is_null_allowed>
           <display_style>list</display_style>
         </field>
+        <field id="redundancy" xsi:type="AttributeRedundancySettings">
+          <sql>redundancy</sql>
+          <relation_code>impacts</relation_code>
+          <from_class>FunctionalCI</from_class>
+          <neighbour_id>applicationsolution</neighbour_id>
+          <enabled>false</enabled>
+          <enabled_mode>user</enabled_mode>
+          <min_up>1</min_up>
+          <min_up_mode>user</min_up_mode>
+          <min_up_type>count</min_up_type>
+        </field>
       </fields>
       <methods/>
       <presentation>

+ 15 - 1
datamodels/2.x/itop-config-mgmt/en.dict.itop-config-mgmt.php

@@ -480,6 +480,11 @@ Dict::Add('EN US', 'English', 'English', array(
 	'Class:DatacenterDevice/Attribute:fiberinterfacelist_list+' => 'All the fiber channel interfaces for this device',
 	'Class:DatacenterDevice/Attribute:san_list' => 'SANs',
 	'Class:DatacenterDevice/Attribute:san_list+' => 'All the SAN switches connected to this device',
+	'Class:DatacenterDevice/Attribute:redundancy' => 'Redundancy',
+	'Class:DatacenterDevice/Attribute:redundancy/count' => 'The device is up if at least one power connection (A or B) is up',
+	// Unused yet
+	'Class:DatacenterDevice/Attribute:redundancy/disabled' => 'The device is up if all its power connections are up',
+	'Class:DatacenterDevice/Attribute:redundancy/percent' => 'The device is up if at least %1$s %% of its power connections are up',
 ));
 
 //
@@ -690,6 +695,10 @@ Dict::Add('EN US', 'English', 'English', array(
 	'Class:ApplicationSolution/Attribute:status/Value:active+' => 'active',
 	'Class:ApplicationSolution/Attribute:status/Value:inactive' => 'inactive',
 	'Class:ApplicationSolution/Attribute:status/Value:inactive+' => 'inactive',
+	'Class:ApplicationSolution/Attribute:redundancy' => 'Impact analysis: configuration of the redundancy',
+	'Class:ApplicationSolution/Attribute:redundancy/disabled' => 'The solution is up is all CIs are up',
+	'Class:ApplicationSolution/Attribute:redundancy/count' => 'The solution is up if at least %1$s CI(s) is(are) up',
+	'Class:ApplicationSolution/Attribute:redundancy/percent' => 'The solution is up if at least %1$s %% of the CIs are up',
 ));
 
 //
@@ -889,6 +898,10 @@ Dict::Add('EN US', 'English', 'English', array(
 	'Class:Farm+' => '',
 	'Class:Farm/Attribute:hypervisor_list' => 'Hypervisors',
 	'Class:Farm/Attribute:hypervisor_list+' => 'All the hypervisors that compose this farm',
+	'Class:Farm/Attribute:redundancy' => 'High availability',
+	'Class:Farm/Attribute:redundancy/disabled' => 'The farm is up if all the hypervisors are up',
+	'Class:Farm/Attribute:redundancy/count' => 'The farm is up if at least %1$s hypervisor(s) is(are) up',
+	'Class:Farm/Attribute:redundancy/percent' => 'The farm is up if at least %1$s %% of the hypervisors are up',
 ));
 
 //
@@ -1861,9 +1874,10 @@ Dict::Add('EN US', 'English', 'English', array(
 
 Dict::Add('EN US', 'English', 'English', array(
 'Server:baseinfo' => 'General information',
-'Server:Date' => 'Date',
+'Server:Date' => 'Dates',
 'Server:moreinfo' => 'More information',
 'Server:otherinfo' => 'Other information',
+'Server:power' => 'Power supply',
 'Person:info' => 'General information',
 'Person:notifiy' => 'Notification',
 'Class:Subnet/Tab:IPUsage' => 'IP Usage',

+ 16 - 2
datamodels/2.x/itop-config-mgmt/fr.dict.itop-config-mgmt.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -425,6 +425,11 @@ Dict::Add('FR FR', 'French', 'Français', array(
 	'Class:DatacenterDevice/Attribute:fiberinterfacelist_list+' => '',
 	'Class:DatacenterDevice/Attribute:san_list' => 'SANs',
 	'Class:DatacenterDevice/Attribute:san_list+' => '',
+	'Class:DatacenterDevice/Attribute:redundancy' => 'Redondance',
+	'Class:DatacenterDevice/Attribute:redundancy/count' => 'Le %2$s est alimenté si au moins une source électrique (A ou B) est opérationnelle',
+	// Unused yet
+	'Class:DatacenterDevice/Attribute:redundancy/disabled' => 'Le %2$s est alimenté si toutes ses sources électriques sont opérationnelles',
+	'Class:DatacenterDevice/Attribute:redundancy/percent' => 'Le %2$s est alimenté si au moins %1$s %% de ses sources électriques sont opérationnelles',
 ));
 
 //
@@ -635,6 +640,10 @@ Dict::Add('FR FR', 'French', 'Français', array(
 	'Class:ApplicationSolution/Attribute:status/Value:active+' => 'active',
 	'Class:ApplicationSolution/Attribute:status/Value:inactive' => 'inactive',
 	'Class:ApplicationSolution/Attribute:status/Value:inactive+' => 'inactive',
+	'Class:ApplicationSolution/Attribute:redundancy' => 'Analyse d\'impact : configuration de la redondance',
+	'Class:ApplicationSolution/Attribute:redundancy/disabled' => 'La solution est opérationelle si tous les CIs qui la composent sont opérationnels',
+	'Class:ApplicationSolution/Attribute:redundancy/count' => 'Nombre minimal de CIs pour que la solution soit opérationnelle : %1$s',
+	'Class:ApplicationSolution/Attribute:redundancy/percent' => 'Pourcentage minimal de CIs pour que la solution soit opérationnelle : %1$s %%',
 ));
 
 //
@@ -833,6 +842,10 @@ Dict::Add('FR FR', 'French', 'Français', array(
 	'Class:Farm+' => '',
 	'Class:Farm/Attribute:hypervisor_list' => 'Hyperviseurs',
 	'Class:Farm/Attribute:hypervisor_list+' => '',
+	'Class:Farm/Attribute:redundancy' => 'Haute disponibilité',
+	'Class:Farm/Attribute:redundancy/disabled' => 'Le vCluster est opérationnel si tous les hyperviseurs qui le composent sont opérationnels',
+	'Class:Farm/Attribute:redundancy/count' => 'Nombre minimal d\'hyperviseurs pour que le vCluster soit opérationnel : %1$s',
+	'Class:Farm/Attribute:redundancy/percent' => 'Pourcentage minimal d\'hyperviseurs pour que le vCluster soit opérationnel : %1$s %%',
 ));
 
 //
@@ -1831,9 +1844,10 @@ Dict::Add('FR FR', 'French', 'Français', array(
 
 Dict::Add('FR FR', 'French', 'Français', array(
 'Server:baseinfo' => 'Informations générales',
-'Server:Date' => 'Date',
+'Server:Date' => 'Dates',
 'Server:moreinfo' => 'Informations complémentaires',
 'Server:otherinfo' => 'Autres informations',
+'Server:power' => 'Alimentation électrique',
 'Person:info' => 'Informations générales',
 'Person:notifiy' => 'Notification',
 'Class:Subnet/Tab:IPUsage' => 'IP utilisées',

+ 0 - 11
datamodels/2.x/itop-datacenter-mgmt/datamodel.itop-datacenter-mgmt.xml

@@ -533,17 +533,6 @@
             <neighbour id="datacenterdevice">
               <query_down>SELECT DatacenterDevice WHERE powerA_id = :this-&gt;id OR powerB_id = :this-&gt;id</query_down>
               <query_up>SELECT PowerConnection WHERE id = :this-&gt;powerA_id OR id = :this-&gt;powerB_id</query_up>
-              <redundancy>
-                <enabled>
-                  <value>true</value>
-                  <mode>fixed</mode>
-                </enabled>
-                <min_up>
-                  <type>count</type>
-                  <value>1</value>
-                  <mode>fixed</mode>
-                </min_up>
-              </redundancy>
             </neighbour>
             <neighbour id="pdu">
               <query_down>SELECT PDU WHERE powerstart_id = :this-&gt;id</query_down>

+ 40 - 8
datamodels/2.x/itop-storage-mgmt/datamodel.itop-storage-mgmt.xml

@@ -142,7 +142,7 @@
                     </item>
                   </items>
                 </item>
-                <item id="fieldset:Server:otherinfo">
+                <item id="fieldset:Server:power">
                   <rank>20</rank>
                   <items>
                     <item id="powerA_id">
@@ -151,11 +151,19 @@
                     <item id="powerB_id">
                       <rank>20</rank>
                     </item>
-                    <item id="description">
+                    <item id="redundancy">
                       <rank>30</rank>
                     </item>
                   </items>
                 </item>
+                <item id="fieldset:Server:otherinfo">
+                  <rank>30</rank>
+                  <items>
+                    <item id="description">
+                      <rank>10</rank>
+                    </item>
+                  </items>
+                </item>
               </items>
             </item>
           </items>
@@ -396,7 +404,7 @@
                     </item>
                   </items>
                 </item>
-                <item id="fieldset:Server:otherinfo">
+                <item id="fieldset:Server:power">
                   <rank>20</rank>
                   <items>
                     <item id="powerA_id">
@@ -405,11 +413,19 @@
                     <item id="powerB_id">
                       <rank>20</rank>
                     </item>
-                    <item id="description">
+                    <item id="redundancy">
                       <rank>30</rank>
                     </item>
                   </items>
                 </item>
+                <item id="fieldset:Server:otherinfo">
+                  <rank>30</rank>
+                  <items>
+                    <item id="description">
+                      <rank>10</rank>
+                    </item>
+                  </items>
+                </item>
               </items>
             </item>
           </items>
@@ -649,7 +665,7 @@
                     </item>
                   </items>
                 </item>
-                <item id="fieldset:Server:otherinfo">
+                <item id="fieldset:Server:power">
                   <rank>20</rank>
                   <items>
                     <item id="powerA_id">
@@ -658,11 +674,19 @@
                     <item id="powerB_id">
                       <rank>20</rank>
                     </item>
-                    <item id="description">
+                    <item id="redundancy">
                       <rank>30</rank>
                     </item>
                   </items>
                 </item>
+                <item id="fieldset:Server:otherinfo">
+                  <rank>30</rank>
+                  <items>
+                    <item id="description">
+                      <rank>10</rank>
+                    </item>
+                  </items>
+                </item>
               </items>
             </item>
           </items>
@@ -902,7 +926,7 @@
                     </item>
                   </items>
                 </item>
-                <item id="fieldset:Server:otherinfo">
+                <item id="fieldset:Server:power">
                   <rank>20</rank>
                   <items>
                     <item id="powerA_id">
@@ -911,11 +935,19 @@
                     <item id="powerB_id">
                       <rank>20</rank>
                     </item>
-                    <item id="description">
+                    <item id="redundancy">
                       <rank>30</rank>
                     </item>
                   </items>
                 </item>
+                <item id="fieldset:Server:otherinfo">
+                  <rank>30</rank>
+                  <items>
+                    <item id="description">
+                      <rank>10</rank>
+                    </item>
+                  </items>
+                </item>
               </items>
             </item>
           </items>

+ 11 - 11
datamodels/2.x/itop-virtualization-mgmt/datamodel.itop-virtualization-mgmt.xml

@@ -407,17 +407,6 @@
           <neighbours>
             <neighbour id="farm">
               <attribute>farm_id</attribute>
-              <redundancy>
-                <enabled>
-                  <value>true</value>
-                  <mode>user</mode>
-                </enabled>
-                <min_up>
-                  <type>count</type>
-                  <value>1</value>
-                  <mode>user</mode>
-                </min_up>
-              </redundancy>
             </neighbour>
           </neighbours>
         </relation>
@@ -455,6 +444,17 @@
           <count_min>0</count_min>
           <count_max>0</count_max>
         </field>
+        <field id="redundancy" xsi:type="AttributeRedundancySettings">
+          <sql>redundancy</sql>
+          <relation_code>impacts</relation_code>
+          <from_class>Hypervisor</from_class>
+          <neighbour_id>farm</neighbour_id>
+          <enabled>true</enabled>
+          <enabled_mode>user</enabled_mode>
+          <min_up>1</min_up>
+          <min_up_type>count</min_up_type>
+          <min_up_mode>user</min_up_mode>
+        </field>
       </fields>
       <methods/>
       <presentation>

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

@@ -346,6 +346,57 @@ function ValidateCaseLogField(sFieldId, bMandatory, sFormId)
 	ReportFieldValidationStatus(sFieldId, sFormId, bValid, '');
 	return bValid;
 }
+
+// Validate the inputs depending on the current setting
+function ValidateRedundancySettings(sFieldId, sFormId)
+{
+	var bValid = true;
+	var sExplain = '';
+
+	$('#'+sFieldId+' :input[type="radio"]:checked').parent().find(':input[type="string"]').each(function (){
+		var sValue = $(this).val().trim();
+		if (sValue == '')
+		{
+			bValid = false;
+			sExplain = Dict.S('UI:ValueMustBeSet');
+		}
+		else
+		{
+			// There is something... check if it is a number
+			re = new RegExp('^[0-9]+$');
+			bValid = re.test(sValue);
+			if (bValid)
+			{
+				var iValue = parseInt(sValue , 10);
+				if ($(this).hasClass('redundancy-min-up-percent'))
+				{
+					// A percentage
+					if ((iValue < 0) || (iValue > 100))
+					{
+						bValid = false;
+					}
+				}
+				else if ($(this).hasClass('redundancy-min-up-count'))
+				{
+					// A count
+					if (iValue < 0)
+					{
+						bValid = false;
+					}
+				}
+
+			}
+			if (!bValid)
+			{
+				sExplain = Dict.S('UI:ValueInvalidFormat');
+			}
+		}
+	});
+
+	ReportFieldValidationStatus(sFieldId, sFormId, bValid, sExplain);
+	return bValid;
+}
+
 // Manage a 'duration' field
 function UpdateDuration(iId)
 {

+ 4 - 0
js/utils.js

@@ -279,10 +279,14 @@ function ToogleField(value, field_id)
 	if (value)
 	{
 		$('#'+field_id).removeAttr('disabled');
+		// In case the field is rendered as a div containing several inputs (e.g. RedundancySettings)
+		$('#'+field_id+' :input').removeAttr('disabled');
 	}
 	else
 	{
 		$('#'+field_id).attr('disabled', 'disabled');
+		// In case the field is rendered as a div containing several inputs (e.g. RedundancySettings)
+		$('#'+field_id+' :input').attr('disabled', 'disabled');
 	}
 	$('#'+field_id).trigger('update');
 	$('#'+field_id).trigger('validate');

+ 33 - 15
setup/compiler.class.inc.php

@@ -670,6 +670,16 @@ EOF;
 		return $val == 'true' ? 'true' : 'false';
 	}
 
+	protected function GetMandatoryPropBoolean($oNode, $sTag)
+	{
+		$val = $oNode->GetChildText($sTag);
+		if (is_null($val))
+		{
+			throw new DOMFormatException("missing (or empty) mandatory tag '$sTag' under the tag '".$oNode->nodeName."'");
+		}
+		return $val == 'true' ? 'true' : 'false';
+	}
+
 	protected function GetPropNumber($oNode, $sTag, $nDefault = null)
 	{
 		$val = $oNode->GetChildText($sTag);
@@ -687,6 +697,16 @@ EOF;
 		return (string)$val;
 	}
 
+	protected function GetMandatoryPropNumber($oNode, $sTag)
+	{
+		$val = $oNode->GetChildText($sTag);
+		if (is_null($val))
+		{
+			throw new DOMFormatException("missing (or empty) mandatory tag '$sTag' under the tag '".$oNode->nodeName."'");
+		}
+		return (string)$val;
+	}
+
 	/**
 	 * Adds quotes and escape characters
 	 */	 	
@@ -1110,6 +1130,18 @@ EOF;
 					$aParameters['target_attcode'] = $this->GetMandatoryPropString($oField, 'target_attcode');
 					$aParameters['item_code'] = $this->GetMandatoryPropString($oField, 'item_code');
 				}
+				elseif ($sAttType == 'AttributeRedundancySettings')
+				{
+					$aParameters['sql'] = $this->GetMandatoryPropString($oField, 'sql');
+					$aParameters['relation_code'] = $this->GetMandatoryPropString($oField, 'relation_code');
+					$aParameters['from_class'] = $this->GetMandatoryPropString($oField, 'from_class');
+					$aParameters['neighbour_id'] = $this->GetMandatoryPropString($oField, 'neighbour_id');
+					$aParameters['enabled'] = $this->GetMandatoryPropBoolean($oField, 'enabled');
+					$aParameters['enabled_mode'] = $this->GetMandatoryPropString($oField, 'enabled_mode');
+					$aParameters['min_up'] = $this->GetMandatoryPropNumber($oField, 'min_up');
+					$aParameters['min_up_mode'] = $this->GetMandatoryPropString($oField, 'min_up_mode');
+					$aParameters['min_up_type'] = $this->GetMandatoryPropString($oField, 'min_up_type');
+				}
 				else
 				{
 					$aParameters['allowed_values'] = 'null'; // or "new ValueSetEnum('SELECT xxxx')"
@@ -1436,7 +1468,7 @@ EOF;
 					{
 						throw new DOMFormatException("Relation '$sRelationId/$sNeighbourId': both a query and and attribute have been specified... which one should be used?");
 					}
-					$aData = array(
+					$aRelations[$sRelationId][$sNeighbourId] = array(
 						'_legacy_' => false,
 						'sDefinedInClass' => $sClass,
 						'sNeighbour' => $sNeighbourId,
@@ -1444,20 +1476,6 @@ EOF;
 						'sQueryUp' => $oNeighbour->GetChildText('query_up'),
 						'sAttribute' => $oNeighbour->GetChildText('attribute'),
 					);
-
-					$oRedundancy = $oNeighbour->GetOptionalElement('redundancy');
-					if ($oRedundancy)
-					{
-						$oEnabled = $oRedundancy->GetUniqueElement('enabled');
-						$aData['bRedundancyEnabledValue'] = ($oEnabled->GetChildText('value', 'false') == 'true');
-						$aData['sRedundancyEnabledMode'] = $oEnabled->GetChildText('mode', 'fixed');
-						$oMinUp = $oRedundancy->GetUniqueElement('min_up');
-						$aData['sRedundancyMinUpType'] = $oMinUp->GetChildText('type', 'count');
-						$aData['iRedundancyMinUpValue'] = (int) $oMinUp->GetChildText('value', 1);
-						$aData['sRedundancyMinUpMode'] = $oMinUp->GetChildText('mode', 'fixed');
-					}
-
-					$aRelations[$sRelationId][$sNeighbourId] = $aData;
 				}
 			}