diff --git a/core/ormlinkset.class.inc.php b/core/ormlinkset.class.inc.php index 2f5037af0..8639f3efe 100644 --- a/core/ormlinkset.class.inc.php +++ b/core/ormlinkset.class.inc.php @@ -123,7 +123,8 @@ class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator { assert($oLink instanceof $this->sClass); // No impact on the iteration algorithm - $this->aAdded[] = $oLink; + $iObjectId = $oLink->GetKey(); + $this->aAdded[$iObjectId] = $oLink; $this->bHasDelta = true; } @@ -148,6 +149,13 @@ class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator $this->aRemoved[$iObjectId] = $iObjectId; $this->bHasDelta = true; } + else + { + if (array_key_exists($iObjectId, $this->aAdded)) + { + unset($this->aAdded[$iObjectId]); + } + } } /** @@ -587,7 +595,7 @@ class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator $oSearch->AddCondition($sExtKeyToMe, $oHostObject->GetKey(), '='); $oSearch->AddCondition($sExtKeyToRemote, $aCheckRemote, 'IN'); $oSet = new DBObjectSet($oSearch); - $aExistingRemote = $oSet->GetColumnAsArray($sExtKeyToRemote); + $aExistingRemote = $oSet->GetColumnAsArray($sExtKeyToRemote, true); } // Write the links according to the existing links @@ -601,11 +609,28 @@ class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator { if (count($aCheckRemote) > 0) { - if (in_array($oLink->Get($sExtKeyToRemote), $aExistingRemote)) - { - // Do not create a duplicate - continue; - } + $bIsDuplicate = false; + foreach($aExistingRemote as $sLinkKey => $sExtKey) + { + if ($sExtKey == $oLink->Get($sExtKeyToRemote)) + { + // Do not create a duplicate + // + In the case of a remove action followed by an add action + // of an existing link, + // the final state to consider is add action, + // so suppress the entry in the removed list. + if (array_key_exists($sLinkKey, $this->aRemoved)) + { + unset($this->aRemoved[$sLinkKey]); + } + $bIsDuplicate = true; + break; + } + } + if ($bIsDuplicate) + { + continue; + } } } diff --git a/datamodels/2.x/itop-tickets/main.itop-tickets.php b/datamodels/2.x/itop-tickets/main.itop-tickets.php index b10abdf35..7126eba0b 100755 --- a/datamodels/2.x/itop-tickets/main.itop-tickets.php +++ b/datamodels/2.x/itop-tickets/main.itop-tickets.php @@ -1,287 +1,286 @@ - - - -/** - * Base class for computing TTO or TTR on a ticket - */ -class ResponseTicketSLT -{ - /** - * Determines the shortest SLT, for this ticket, for the given metric. Returns null is no SLT was found - * @param string $sMetric Type of metric 'TTO', 'TTR', etc as defined in the SLT class - * @return hash Array with 'SLT' => name of the SLT selected, 'value' => duration in seconds of the SLT metric, null if no SLT applies to this ticket - */ - protected static function ComputeSLT($oTicket, $sMetric = 'TTO') - { - $iDeadline = null; - if (MetaModel::IsValidClass('SLT')) - { - $sType=get_class($oTicket); - if ($sType == 'Incident') - { - $sRequestType = 'incident'; - } - else - { - $sRequestType = $oTicket->Get('request_type'); - } - - $aArgs = $oTicket->ToArgs(); - $aArgs['metric'] = $sMetric; - $aArgs['request_type'] = $sRequestType; - - //echo "
Managing:".$sMetric."-".$this->Get('request_type')."-".$this->Get('importance')."
\n"; - $oSLTSet = new DBObjectSet(DBObjectSearch::FromOQL(RESPONSE_TICKET_SLT_QUERY), - array(), - $aArgs - ); - - $iMinDuration = PHP_INT_MAX; - $sSLTName = ''; - - while($oSLT = $oSLTSet->Fetch()) - { - $iDuration = (int)$oSLT->Get('value'); - $sUnit = $oSLT->Get('unit'); - switch($sUnit) - { - case 'days': - $iDuration = $iDuration * 24; // 24 hours in 1 days - // Fall though - - case 'hours': - $iDuration = $iDuration * 60; // 60 minutes in 1 hour - // Fall though - - case 'minutes': - $iDuration = $iDuration * 60; - } - if ($iDuration < $iMinDuration) - { - $iMinDuration = $iDuration; - $sSLTName = $oSLT->GetName(); - } - } - if ($iMinDuration == PHP_INT_MAX) - { - $iDeadline = null; - } - else - { - // Store $sSLTName to keep track of which SLT has been used - $iDeadline = $iMinDuration; - } - } - return $iDeadline; - - } -} - -/** - * Compute the TTO of a ticket - null if the class 'SLT' does not exist - */ -class ResponseTicketTTO extends ResponseTicketSLT implements iMetricComputer -{ - public static function GetDescription() - { - return "Time to own a ticket"; - } - - public function ComputeMetric($oObject) - { - $iRes = $this->ComputeSLT($oObject, 'TTO'); - return $iRes; - } -} - -/** - * Compute the TTR of a ticket - null if the class 'SLT' does not exist - */ -class ResponseTicketTTR extends ResponseTicketSLT implements iMetricComputer -{ - public static function GetDescription() - { - return "Time to resolve a ticket"; - } - - public function ComputeMetric($oObject) - { - $iRes = $this->ComputeSLT($oObject, 'TTR'); - return $iRes; - } -} - - -class _Ticket extends cmdbAbstractObject -{ - - public function UpdateImpactedItems() - { - require_once(APPROOT.'core/displayablegraph.class.inc.php'); - $oContactsSet = $this->Get('contacts_list'); - $oCIsSet = $this->Get('functionalcis_list'); - - $aCIsToImpactCode = array(); - $aSources = array(); - $aExcluded = array(); - - $oCIsSet->Rewind(); - while ($oLink = $oCIsSet->Fetch()) - { - $iKey = $oLink->Get('functionalci_id'); - $aCIsToImpactCode[$iKey] = $oLink->Get('impact_code'); - if ($oLink->Get('impact_code') == 'manual') - { - $oObj = MetaModel::GetObject('FunctionalCI', $iKey); - $aSources[$iKey] = $oObj; - } - else if ($oLink->Get('impact_code') == 'not_impacted') - { - $oObj = MetaModel::GetObject('FunctionalCI', $iKey); - $aExcluded[$iKey] = $oObj; - } - } - - $aContactsToRoleCode = array(); - $oContactsSet->Rewind(); - while ($oLink = $oContactsSet->Fetch()) - { - $iKey = $oLink->Get('contact_id'); - $aContactsToRoleCode[$iKey] = $oLink->Get('role_code'); - if ($oLink->Get('role_code') == 'do_not_notify') - { - $oObj = MetaModel::GetObject('Contact', $iKey); - $aExcluded[$iKey] = $oObj; - } - } - - $oNewCIsSet = DBObjectSet::FromScratch('lnkFunctionalCIToTicket'); - foreach($aCIsToImpactCode as $iKey => $sImpactCode) - { - if ($sImpactCode != 'computed') - { - $oNewLink = new lnkFunctionalCIToTicket(); - $oNewLink->Set('functionalci_id', $iKey); - $oNewLink->Set('impact_code', $sImpactCode); - $oNewCIsSet->AddObject($oNewLink); - } - } - - $oNewContactsSet = DBObjectSet::FromScratch('lnkContactToTicket'); - foreach($aContactsToRoleCode as $iKey => $sImpactCode) - { - if ($sImpactCode != 'computed') - { - $oNewLink = new lnkContactToTicket(); - $oNewLink->Set('contact_id', $iKey); - $oNewLink->Set('role_code', $sImpactCode); - $oNewContactsSet->AddObject($oNewLink); - } - } - - $sContextKey = 'itop-tickets/relation_context/'.get_class($this).'/impacts/down'; - $aContextDefs = DisplayableGraph::GetContextDefinitions($sContextKey, true, array('this' => $this)); - $aDefaultContexts = array(); - foreach($aContextDefs as $sKey => $aDefinition) - { - // Add the default context queries to the computation - if (array_key_exists('default', $aDefinition) && ($aDefinition['default'] == 'yes')) - { - $aDefaultContexts[] = $aDefinition['oql']; - } - } - // Merge the directly impacted items with the "new" ones added by the "context" queries - $aGraphObjects = array(); - $oRawGraph = MetaModel::GetRelatedObjectsDown('impacts', $aSources, 10, true /* bEnableRedundancy */, $aExcluded); - $oIterator = new RelationTypeIterator($oRawGraph, 'Node'); - foreach ($oIterator as $oNode) - { - // Any object node reached AND different from a source will do - if ( ($oNode instanceof RelationObjectNode) && ($oNode->GetProperty('is_reached')) && (!$oNode->GetProperty('source')) ) - { - $oObj = $oNode->GetProperty('object'); - $iKey = $oObj->GetKey(); - $aGraphObjects[get_class($oObj).'::'.$iKey] = $oNode->GetProperty('object'); - } - } - - if (count($aDefaultContexts) > 0) - { - $oAnnotatedGraph = MetaModel::GetRelatedObjectsDown('impacts', $aSources, 10, true /* bEnableRedundancy */, $aExcluded, $aDefaultContexts); - $oIterator = new RelationTypeIterator($oAnnotatedGraph, 'Node'); - foreach ($oIterator as $oNode) - { - // Only pick the nodes which are NOT impacted by a context root cause, and merge them in the list - if ( ($oNode instanceof RelationObjectNode) && ($oNode->GetProperty('is_reached')) && (!$oNode->GetProperty('source')) && ($oNode->GetProperty('context_root_causes', null) == null) ) - { - $oObj = $oNode->GetProperty('object'); - $iKey = $oObj->GetKey(); - $aGraphObjects[get_class($oObj).'::'.$iKey] = $oNode->GetProperty('object'); - } - } - } - - foreach ($aGraphObjects as $oObj) - { - $iKey = $oObj->GetKey(); - $sRootClass = MetaModel::GetRootClass(get_class($oObj)); - switch ($sRootClass) - { - case 'FunctionalCI': - // Only FunctionalCIs which are not already linked to the ticket - if (!array_key_exists($iKey, $aCIsToImpactCode) || ($aCIsToImpactCode[$iKey] != 'not_impacted')) - { - $oNewLink = new lnkFunctionalCIToTicket(); - $oNewLink->Set('functionalci_id', $iKey); - $oNewLink->Set('impact_code', 'computed'); - $oNewCIsSet->AddObject($oNewLink); - } - break; - - case 'Contact': - // Only link Contacts which are not already linked to the ticket - if (!array_key_exists($iKey, $aContactsToRoleCode) || ($aContactsToRoleCode[$iKey] != 'do_not_notify')) - { - $oNewLink = new lnkContactToTicket(); - $oNewLink->Set('contact_id', $iKey); - $oNewLink->Set('role_code', 'computed'); - $oNewContactsSet->AddObject($oNewLink); - } - break; - } - } - $this->Set('functionalcis_list', $oNewCIsSet); - $this->Set('contacts_list', $oNewContactsSet); - } - - public function DisplayBareRelations(WebPage $oPage, $bEditMode = false) - { - parent::DisplayBareRelations($oPage, $bEditMode); - // Display the impact analysis for tickets not in 'closed' or 'resolved' status... and not in edition - if ((!$bEditMode) && (!in_array($this->Get('status'), array('resolved', 'closed')))) - { - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/fraphael.js'); - $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/jquery.contextMenu.css'); - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.contextMenu.js'); - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/simple_graph.js'); - $oPage->AddAjaxTab(Dict::S('Ticket:ImpactAnalysis'), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=ticket_impact&class='.get_class($this).'&id='.$this->GetKey(), true); - } - } -} + + + +/** + * Base class for computing TTO or TTR on a ticket + */ +class ResponseTicketSLT +{ + /** + * Determines the shortest SLT, for this ticket, for the given metric. Returns null is no SLT was found + * @param string $sMetric Type of metric 'TTO', 'TTR', etc as defined in the SLT class + * @return hash Array with 'SLT' => name of the SLT selected, 'value' => duration in seconds of the SLT metric, null if no SLT applies to this ticket + */ + protected static function ComputeSLT($oTicket, $sMetric = 'TTO') + { + $iDeadline = null; + if (MetaModel::IsValidClass('SLT')) + { + $sType=get_class($oTicket); + if ($sType == 'Incident') + { + $sRequestType = 'incident'; + } + else + { + $sRequestType = $oTicket->Get('request_type'); + } + + $aArgs = $oTicket->ToArgs(); + $aArgs['metric'] = $sMetric; + $aArgs['request_type'] = $sRequestType; + + //echo "Managing:".$sMetric."-".$this->Get('request_type')."-".$this->Get('importance')."
\n"; + $oSLTSet = new DBObjectSet(DBObjectSearch::FromOQL(RESPONSE_TICKET_SLT_QUERY), + array(), + $aArgs + ); + + $iMinDuration = PHP_INT_MAX; + $sSLTName = ''; + + while($oSLT = $oSLTSet->Fetch()) + { + $iDuration = (int)$oSLT->Get('value'); + $sUnit = $oSLT->Get('unit'); + switch($sUnit) + { + case 'days': + $iDuration = $iDuration * 24; // 24 hours in 1 days + // Fall though + + case 'hours': + $iDuration = $iDuration * 60; // 60 minutes in 1 hour + // Fall though + + case 'minutes': + $iDuration = $iDuration * 60; + } + if ($iDuration < $iMinDuration) + { + $iMinDuration = $iDuration; + $sSLTName = $oSLT->GetName(); + } + } + if ($iMinDuration == PHP_INT_MAX) + { + $iDeadline = null; + } + else + { + // Store $sSLTName to keep track of which SLT has been used + $iDeadline = $iMinDuration; + } + } + return $iDeadline; + + } +} + +/** + * Compute the TTO of a ticket - null if the class 'SLT' does not exist + */ +class ResponseTicketTTO extends ResponseTicketSLT implements iMetricComputer +{ + public static function GetDescription() + { + return "Time to own a ticket"; + } + + public function ComputeMetric($oObject) + { + $iRes = $this->ComputeSLT($oObject, 'TTO'); + return $iRes; + } +} + +/** + * Compute the TTR of a ticket - null if the class 'SLT' does not exist + */ +class ResponseTicketTTR extends ResponseTicketSLT implements iMetricComputer +{ + public static function GetDescription() + { + return "Time to resolve a ticket"; + } + + public function ComputeMetric($oObject) + { + $iRes = $this->ComputeSLT($oObject, 'TTR'); + return $iRes; + } +} + + +class _Ticket extends cmdbAbstractObject +{ + + public function UpdateImpactedItems() + { + require_once(APPROOT.'core/displayablegraph.class.inc.php'); + + /** @var ormLinkSet $oContactsSet */ + $oContactsSet = $this->Get('contacts_list'); + /** @var ormLinkSet $oCIsSet */ + $oCIsSet = $this->Get('functionalcis_list'); + + $aCIsToImpactCode = array(); + $aSources = array(); + $aExcluded = array(); + foreach ($oCIsSet as $oLink) + { + $iKey = $oLink->Get('functionalci_id'); + $aCIsToImpactCode[$iKey] = array('link' => $oLink->GetKey(), 'code' => $oLink->Get('impact_code')); + if ($oLink->Get('impact_code') == 'manual') + { + $oObj = MetaModel::GetObject('FunctionalCI', $iKey); + $aSources[$iKey] = $oObj; + } + else if ($oLink->Get('impact_code') == 'not_impacted') + { + $oObj = MetaModel::GetObject('FunctionalCI', $iKey); + $aExcluded[] = $oObj; + } + } + + $aContactsToRoleCode = array(); + foreach ($oContactsSet as $oLink) + { + $iKey = $oLink->Get('contact_id'); + $aContactsToRoleCode[$iKey] = array('link' => $oLink->GetKey(), 'code' => $oLink->Get('role_code')); + if ($oLink->Get('role_code') == 'do_not_notify') + { + $oObj = MetaModel::GetObject('Contact', $iKey); + $aExcluded[] = $oObj; + } + } + + $sContextKey = 'itop-tickets/relation_context/'.get_class($this).'/impacts/down'; + $aContextDefs = DisplayableGraph::GetContextDefinitions($sContextKey, true, array('this' => $this)); + $aDefaultContexts = array(); + foreach($aContextDefs as $sKey => $aDefinition) + { + // Add the default context queries to the computation + if (array_key_exists('default', $aDefinition) && ($aDefinition['default'] == 'yes')) + { + $aDefaultContexts[] = $aDefinition['oql']; + } + } + // Merge the directly impacted items with the "new" ones added by the "context" queries + $aGraphObjects = array(); + $oRawGraph = MetaModel::GetRelatedObjectsDown('impacts', $aSources, 10, true /* bEnableRedundancy */, $aExcluded); + $oIterator = new RelationTypeIterator($oRawGraph, 'Node'); + foreach ($oIterator as $oNode) + { + // Any object node reached AND different from a source will do + if ( ($oNode instanceof RelationObjectNode) && ($oNode->GetProperty('is_reached')) && (!$oNode->GetProperty('source')) ) + { + $this->StoreComputedObject($aGraphObjects, $oNode->GetProperty('object')); + } + } + if (count($aDefaultContexts) > 0) + { + $oAnnotatedGraph = MetaModel::GetRelatedObjectsDown('impacts', $aSources, 10, true /* bEnableRedundancy */, $aExcluded, $aDefaultContexts); + $oIterator = new RelationTypeIterator($oAnnotatedGraph, 'Node'); + foreach ($oIterator as $oNode) + { + // Only pick the nodes which are NOT impacted by a context root cause, and merge them in the list + if (($oNode instanceof RelationObjectNode) && ($oNode->GetProperty('is_reached')) && (!$oNode->GetProperty('source')) && ($oNode->GetProperty('context_root_causes', null) == null)) + { + $this->StoreComputedObject($aGraphObjects, $oNode->GetProperty('object')); + } + } + } + + // Remove unnecessary "computed" CIs and Contacts + foreach($aCIsToImpactCode as $iKey => $aCode) + { + if (($aCode['code'] == 'computed') && (!isset($aGraphObjects['FunctionalCI']) || (!array_key_exists($iKey, $aGraphObjects['FunctionalCI'])))) + { + $oCIsSet->RemoveItem($aCode['link']); + } + } + foreach($aContactsToRoleCode as $iKey => $aCode) + { + if (($aCode['code'] == 'computed') && (!isset($aGraphObjects['Contact']) || (!array_key_exists($iKey, $aGraphObjects['Contact'])))) + { + $oContactsSet->RemoveItem($aCode['link']); + } + } + + // Add new nodes + foreach ($aGraphObjects as $sRootClass => $aObjects) + { + switch ($sRootClass) + { + case 'FunctionalCI': + // Only FunctionalCIs which are not already linked to the ticket + foreach($aObjects as $iKey => $oObj) + { + if (!array_key_exists($iKey, $aCIsToImpactCode)) + { + $oNewLink = new lnkFunctionalCIToTicket(); + $oNewLink->Set('functionalci_id', $iKey); + $oNewLink->Set('impact_code', 'computed'); + $oCIsSet->AddItem($oNewLink); + } + } + break; + + case 'Contact': + // Only link Contacts which are not already linked to the ticket + foreach($aObjects as $iKey => $oObj) + { + if (!array_key_exists($iKey, $aContactsToRoleCode)) + { + $oNewLink = new lnkContactToTicket(); + $oNewLink->Set('contact_id', $iKey); + $oNewLink->Set('role_code', 'computed'); + $oContactsSet->AddItem($oNewLink); + } + } + break; + } + } + $this->Set('functionalcis_list', $oCIsSet); + $this->Set('contacts_list', $oContactsSet); + } + + private function StoreComputedObject(&$aGraphObjects, $oObj) + { + $iKey = $oObj->GetKey(); + $sRootClass = MetaModel::GetRootClass(get_class($oObj)); + $aGraphObjects[$sRootClass][$iKey] = $oObj; + } + + public function DisplayBareRelations(WebPage $oPage, $bEditMode = false) + { + parent::DisplayBareRelations($oPage, $bEditMode); + // Display the impact analysis for tickets not in 'closed' or 'resolved' status... and not in edition + if ((!$bEditMode) && (!in_array($this->Get('status'), array('resolved', 'closed')))) + { + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/fraphael.js'); + $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/jquery.contextMenu.css'); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.contextMenu.js'); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/simple_graph.js'); + $oPage->AddAjaxTab(Dict::S('Ticket:ImpactAnalysis'), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=ticket_impact&class='.get_class($this).'&id='.$this->GetKey(), true); + } + } +} ?> \ No newline at end of file