diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index 2e9e48485..922824015 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -1,5 +1,5 @@ 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 = "".self::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $oValue, $sDisplayValue, $sInputId, '', $iFlags, $aArgs).''; $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('
'); + $oPage->add(''.$oRedundancyAttDef->GetLabel().''); + if ($bEditMode && (!$bRedundancyReadOnly)) + { + $sInputId = $this->m_iFormId.'_'.$sRedundancyAttCode; + $oPage->add("".self::GetFormElementForField($oPage, $sClass, $sRedundancyAttCode, $oRedundancyAttDef, $sValue, '', $sInputId, '', $iFlags, $aArgs).''); + } + else + { + $oPage->add($oRedundancyAttDef->GetDisplayForm($sValue, $oPage, false, $this->m_iFormId)); + } + $oPage->add('
'); + } + } } $oPage->SetCurrentTab(''); @@ -527,18 +552,7 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay $sComments = isset($aFieldsComments[$sAttCode]) ? $aFieldsComments[$sAttCode] : ' '; $sInfos = ' '; - 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 .= "\n"; break; + case 'RedundancySetting': + $sHTMLValue = ''; + $sHTMLValue .= ''; + $sHTMLValue .= ''; + $sHTMLValue .= ''; + $sHTMLValue .= ''; + $sHTMLValue .= '
'; + $sHTMLValue .= '
'; + $sHTMLValue .= $oAttDef->GetDisplayForm($value, $oPage, true); + $sHTMLValue .= '
'; + $sHTMLValue .= '
'.$sValidationField.'
'; + $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'; @@ -2632,6 +2660,26 @@ EOF return $aWriteableAttList; } + /** + * 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) @@ -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; + } } -?> diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 354d8464d..a5ca922fd 100644 --- a/core/attributedef.class.inc.php +++ b/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 .= '
'; + $sRet .= $this->GetDisplayOption($sCurrentValue, $oPage, $sFormPrefix, $bEditOption, $sUserOption, $bSelected); + $sRet .= '
'; + } + 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 = ''; + // 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 = ''; + // 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 = ' '; + } + 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; + } +} diff --git a/core/cmdbobject.class.inc.php b/core/cmdbobject.class.inc.php index afe19887c..29685d2e0 100644 --- a/core/cmdbobject.class.inc.php +++ b/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'); diff --git a/core/metamodel.class.php b/core/metamodel.class.php index 03864a1a5..85415de27 100644 --- a/core/metamodel.class.php +++ b/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'"; diff --git a/core/redundancysettings.class.inc.php b/core/redundancysettings.class.inc.php deleted file mode 100644 index d68731bac..000000000 --- a/core/redundancysettings.class.inc.php +++ /dev/null @@ -1,98 +0,0 @@ - - -/** - * 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; - } -} diff --git a/core/relationgraph.class.inc.php b/core/relationgraph.class.inc.php index 1b69d2d68..d1c90cb3e 100644 --- a/core/relationgraph.class.inc.php +++ b/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()])) + $oRet = null; + foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) { - // Cache hit - $oUserSettings = $this->aRedundancySettings[$sNeighbourKey][$oToNode->GetId()]; + if ($oAttDef instanceof AttributeRedundancySettings) + { + if ($oAttDef->Get('relation_code') == $sRelCode) + { + if ($oAttDef->Get('neighbour_id') == $aQueryInfo['sNeighbour']) + { + $oRet = $oAttDef; + break; + } + } + } } - else - { - // Cache miss: build the entry - $oUserSettings = RedundancySettings::GetSettings($sRelCode, $aQueryInfo, $oToNode->GetProperty('object')); - $this->aRedundancySettings[$sNeighbourKey][$oToNode->GetId()] = $oUserSettings; - } - return $oUserSettings; + return $oRet; } } diff --git a/datamodels/2.x/itop-config-mgmt/datamodel.itop-config-mgmt.xml b/datamodels/2.x/itop-config-mgmt/datamodel.itop-config-mgmt.xml index a58deba70..0b882122a 100755 --- a/datamodels/2.x/itop-config-mgmt/datamodel.itop-config-mgmt.xml +++ b/datamodels/2.x/itop-config-mgmt/datamodel.itop-config-mgmt.xml @@ -1472,17 +1472,6 @@ applicationsolution_list - - - false - user - - - count - 1 - user - - softwares_list @@ -2000,6 +1989,17 @@ san_id + + redundancy + impacts + PowerConnection + datacenterdevice + true + fixed + 1 + count + fixed + @@ -2316,7 +2316,7 @@ - + 20 @@ -2325,11 +2325,19 @@ 20 - + 30 + + 30 + + + 10 + + + @@ -2640,7 +2648,7 @@ - + 20 @@ -2649,11 +2657,19 @@ 20 - + 30 + + 30 + + + 10 + + + @@ -2807,6 +2823,17 @@ true list + + redundancy + impacts + FunctionalCI + applicationsolution + false + user + 1 + user + count + diff --git a/datamodels/2.x/itop-config-mgmt/en.dict.itop-config-mgmt.php b/datamodels/2.x/itop-config-mgmt/en.dict.itop-config-mgmt.php index f75226f61..142d91fed 100755 --- a/datamodels/2.x/itop-config-mgmt/en.dict.itop-config-mgmt.php +++ b/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', diff --git a/datamodels/2.x/itop-config-mgmt/fr.dict.itop-config-mgmt.php b/datamodels/2.x/itop-config-mgmt/fr.dict.itop-config-mgmt.php index 505734a22..a7f3e75ee 100755 --- a/datamodels/2.x/itop-config-mgmt/fr.dict.itop-config-mgmt.php +++ b/datamodels/2.x/itop-config-mgmt/fr.dict.itop-config-mgmt.php @@ -1,5 +1,5 @@ '', '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', diff --git a/datamodels/2.x/itop-datacenter-mgmt/datamodel.itop-datacenter-mgmt.xml b/datamodels/2.x/itop-datacenter-mgmt/datamodel.itop-datacenter-mgmt.xml index 86ae19a66..ec09b409c 100755 --- a/datamodels/2.x/itop-datacenter-mgmt/datamodel.itop-datacenter-mgmt.xml +++ b/datamodels/2.x/itop-datacenter-mgmt/datamodel.itop-datacenter-mgmt.xml @@ -533,17 +533,6 @@ SELECT DatacenterDevice WHERE powerA_id = :this->id OR powerB_id = :this->id SELECT PowerConnection WHERE id = :this->powerA_id OR id = :this->powerB_id - - - true - fixed - - - count - 1 - fixed - - SELECT PDU WHERE powerstart_id = :this->id diff --git a/datamodels/2.x/itop-storage-mgmt/datamodel.itop-storage-mgmt.xml b/datamodels/2.x/itop-storage-mgmt/datamodel.itop-storage-mgmt.xml index a2bc90ff7..03b9ac991 100644 --- a/datamodels/2.x/itop-storage-mgmt/datamodel.itop-storage-mgmt.xml +++ b/datamodels/2.x/itop-storage-mgmt/datamodel.itop-storage-mgmt.xml @@ -142,7 +142,7 @@ - + 20 @@ -151,11 +151,19 @@ 20 - + 30 + + 30 + + + 10 + + + @@ -396,7 +404,7 @@ - + 20 @@ -405,11 +413,19 @@ 20 - + 30 + + 30 + + + 10 + + + @@ -649,7 +665,7 @@ - + 20 @@ -658,11 +674,19 @@ 20 - + 30 + + 30 + + + 10 + + + @@ -902,7 +926,7 @@ - + 20 @@ -911,11 +935,19 @@ 20 - + 30 + + 30 + + + 10 + + + diff --git a/datamodels/2.x/itop-virtualization-mgmt/datamodel.itop-virtualization-mgmt.xml b/datamodels/2.x/itop-virtualization-mgmt/datamodel.itop-virtualization-mgmt.xml index 67b658ab2..14f9c79e5 100644 --- a/datamodels/2.x/itop-virtualization-mgmt/datamodel.itop-virtualization-mgmt.xml +++ b/datamodels/2.x/itop-virtualization-mgmt/datamodel.itop-virtualization-mgmt.xml @@ -407,17 +407,6 @@ farm_id - - - true - user - - - count - 1 - user - - @@ -455,6 +444,17 @@ 0 0 + + redundancy + impacts + Hypervisor + farm + true + user + 1 + count + user + diff --git a/js/forms-json-utils.js b/js/forms-json-utils.js index 3b7176888..db5598691 100644 --- a/js/forms-json-utils.js +++ b/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) { diff --git a/js/utils.js b/js/utils.js index 042fc0b4c..d15f875b9 100644 --- a/js/utils.js +++ b/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'); diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index 4c2f65809..3bd43fe53 100644 --- a/setup/compiler.class.inc.php +++ b/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; } }