diff --git a/core/displayablegraph.class.inc.php b/core/displayablegraph.class.inc.php index 19d96b5da..4bdf7ae99 100644 --- a/core/displayablegraph.class.inc.php +++ b/core/displayablegraph.class.inc.php @@ -81,7 +81,7 @@ class DisplayableNode extends GraphNode return sqrt($this->Distance2($oNode)); } - public function GetForRaphael() + public function GetForRaphael($aContextDefs) { $aNode = array(); $aNode['shape'] = 'icon'; @@ -97,18 +97,28 @@ class DisplayableNode extends GraphNode $aNode['id'] = $this->GetId(); $fOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4); $aNode['icon_attr'] = array('opacity' => $fOpacity); - $aNode['text_attr'] = array('opacity' => $fOpacity); + $aNode['text_attr'] = array('opacity' => $fOpacity); + $aNode['tooltip'] = $this->GetTooltip($aContextDefs); + $aNode['context_icons'] = array(); + $aContextRootCauses = $this->GetProperty('context_root_causes'); + if (!is_null($aContextRootCauses)) + { + foreach($aContextRootCauses as $key => $aObjects) + { + $aNode['context_icons'][] = utils::GetAbsoluteUrlModulesRoot().$aContextDefs[$key]['icon']; + } + } return $aNode; } - public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale) + public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs) { $Alpha = 1.0; $oPdf->SetFillColor(200, 200, 200); $oPdf->setAlpha(1); $sIconUrl = $this->GetProperty('icon_url'); - $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-production/', $sIconUrl); + $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-'.utils::GetCurrentEnvironment().'/', $sIconUrl); if ($this->GetProperty('source')) { @@ -134,6 +144,24 @@ class DisplayableNode extends GraphNode $oPdf->Image($sIconPath, ($this->x - 16)*$fScale, ($this->y - 16)*$fScale, 32*$fScale, 32*$fScale); + $aContextRootCauses = $this->GetProperty('context_root_causes'); + if (!is_null($aContextRootCauses)) + { + $idx = 0; + foreach($aContextRootCauses as $key => $aObjects) + { + $sgn = 2*($idx %2) -1; + $coef = floor((1+$idx)/2) * $sgn; + $alpha = $coef*pi()/4 - pi()/2; + $x = $this->x * $fScale + cos($alpha) * 16*1.25 * $fScale; + $y = $this->y * $fScale + sin($alpha) * 16*1.25 * $fScale; + $l = 32 * $fScale / 3; + $sIconPath = APPROOT.'env-'.utils::GetCurrentEnvironment().'/'.$aContextDefs[$key]['icon']; + $oPdf->Image($sIconPath, $x - $l/2, $y - $l/2, $l, $l); + $idx++; + } + } + $oPdf->SetFont('dejavusans', '', 24 * $fScale, '', true); $width = $oPdf->GetStringWidth($this->GetProperty('label')); $height = $oPdf->GetStringHeight(1000, $this->GetProperty('label')); @@ -309,6 +337,38 @@ class DisplayableNode extends GraphNode } } } + + public function GetTooltip($aContextDefs) + { + $sHtml = ''; + $oCurrObj = $this->GetProperty('object'); + $sSubClass = get_class($oCurrObj); + $sHtml .= $oCurrObj->GetHyperlink()."
"; + $aContextRootCauses = $this->GetProperty('context_root_causes'); + if (!is_null($aContextRootCauses)) + { + foreach($aContextRootCauses as $key => $aObjects) + { + //$sHtml .= print_r($aContextDefs, true); + $aContext = $aContextDefs[$key]; + $aRootCauses = array(); + foreach($aObjects as $oRootCause) + { + $aRootCauses[] = $oRootCause->GetHyperlink(); + } + $sHtml .= '

 '.implode(', ', $aRootCauses).'

'; + } + $sHtml .= '
'; + } + $sHtml .= ''; + foreach(MetaModel::GetZListItems($sSubClass, 'list') as $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode); + $sHtml .= ''; + } + $sHtml .= '
'.$oAttDef->GetLabel().': '.$oCurrObj->GetAsHtml($sAttCode).'
'; + return $sHtml; + } } class DisplayableRedundancyNode extends DisplayableNode @@ -318,7 +378,7 @@ class DisplayableRedundancyNode extends DisplayableNode return 24; } - public function GetForRaphael() + public function GetForRaphael($aContextDefs) { $aNode = array(); $aNode['shape'] = 'disc'; @@ -330,16 +390,25 @@ class DisplayableRedundancyNode extends DisplayableNode $aNode['label'] = $this->GetLabel(); $aNode['id'] = $this->GetId(); $fDiscOpacity = ($this->GetProperty('is_reached') ? 1 : 0.2); - $aNode['disc_attr'] = array('stroke-width' => 3, 'stroke' => '#000', 'fill' => '#c33', 'opacity' => $fDiscOpacity); + $sColor = ($this->GetProperty('is_reached_count') > $this->GetProperty('threshold')) ? '#c33' : '#999'; + $aNode['disc_attr'] = array('stroke-width' => 3, 'stroke' => '#000', 'fill' => $sColor, 'opacity' => $fDiscOpacity); $fTextOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4); - $aNode['text_attr'] = array('fill' => '#fff', 'opacity' => $fTextOpacity); + $aNode['text_attr'] = array('fill' => '#fff', 'opacity' => $fTextOpacity); + $aNode['tooltip'] = $this->GetTooltip($aContextDefs); return $aNode; } - public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale) + public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs) { $oPdf->SetAlpha(1); - $oPdf->SetFillColor(200, 0, 0); + if($this->GetProperty('is_reached_count') > $this->GetProperty('threshold')) + { + $oPdf->SetFillColor(200, 0, 0); + } + else + { + $oPdf->SetFillColor(144, 144, 144); + } $oPdf->SetDrawColor(0, 0, 0); $oPdf->Circle($this->x*$fScale, $this->y*$fScale, 16*$fScale, 0, 360, 'DF'); @@ -430,11 +499,22 @@ class DisplayableRedundancyNode extends DisplayableNode } } } + + public function GetTooltip($aContextDefs) + { + $sHtml = ''; + $sHtml .= "Redundancy
"; + $sHtml .= ''; + $sHtml .= ""; + $sHtml .= ""; + $sHtml .= '
# Items Impacted: ".$this->GetProperty('is_reached_count')." / ".($this->GetProperty('min_up') + $this->GetProperty('threshold'))."
Critical Threshold: ".$this->GetProperty('threshold')." / ".($this->GetProperty('min_up') + $this->GetProperty('threshold'))."
'; + return $sHtml; + } } class DisplayableEdge extends GraphEdge { - public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale) + public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs) { $xStart = $this->GetSourceNode()->x * $fScale; $yStart = $this->GetSourceNode()->y * $fScale; @@ -498,7 +578,7 @@ class DisplayableGroupNode extends DisplayableNode return 50; } - public function GetForRaphael() + public function GetForRaphael($aContextDefs) { $aNode = array(); $aNode['shape'] = 'group'; @@ -515,10 +595,11 @@ class DisplayableGroupNode extends DisplayableNode $aNode['icon_attr'] = array('opacity' => $fTextOpacity); $aNode['disc_attr'] = array('stroke-width' => 3, 'stroke' => '#000', 'fill' => '#fff', 'opacity' => $fDiscOpacity); $aNode['text_attr'] = array('fill' => '#000', 'opacity' => $fTextOpacity); + $aNode['tooltip'] = $this->GetTooltip($aContextDefs); return $aNode; } - public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale) + public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs) { $bReached = $this->GetProperty('is_reached'); $oPdf->SetFillColor(255, 255, 255); @@ -533,7 +614,7 @@ class DisplayableGroupNode extends DisplayableNode $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => $aBorderColor)); $sIconUrl = $this->GetProperty('icon_url'); - $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-production/', $sIconUrl); + $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-'.utils::GetCurrentEnvironment().'/', $sIconUrl); $oPdf->SetAlpha(1); $oPdf->Circle($this->x*$fScale, $this->y*$fScale, $this->GetWidth() / 2 * $fScale, 0, 360, 'DF'); @@ -553,6 +634,14 @@ class DisplayableGroupNode extends DisplayableNode $oPdf->SetTextColor(0, 0, 0); $oPdf->Text($this->x*$fScale - $width/2, ($this->y + 25)*$fScale, $this->GetProperty('label')); } + + public function GetTooltip($aContextDefs) + { + $sHtml = ''; + $iGroupIdx = $this->GetProperty('group_index'); + $sHtml .= Dict::Format('UI:RelationGroupNumber_N', (1+$iGroupIdx)); + return $sHtml; + } } /** @@ -633,12 +722,17 @@ class DisplayableGraph extends SimpleGraph $oNewNode->SetProperty('icon_url', $oObj->GetIcon(false)); $oNewNode->SetProperty('label', $oObj->GetRawName()); $oNewNode->SetProperty('is_reached', $bDirectionDown ? $oNode->GetProperty('is_reached') : true); // When going "up" is_reached does not matter - $oNewNode->SetProperty('developped', $oNode->GetProperty('developped')); + $oNewNode->SetProperty('is_reached_allowed', $oNode->GetProperty('is_reached_allowed')); + $oNewNode->SetProperty('context_root_causes', $oNode->GetProperty('context_root_causes')); break; default: $oNewNode = new DisplayableRedundancyNode($oNewGraph, $oNode->GetId(), 0, 0); - $oNewNode->SetProperty('label', $oNode->GetProperty('min_up')); + $iNbReached = (is_null($oNode->GetProperty('is_reached_count'))) ? 0 : $oNode->GetProperty('is_reached_count'); + $oNewNode->SetProperty('label', $iNbReached."/".($oNode->GetProperty('min_up') + $oNode->GetProperty('threshold'))); + $oNewNode->SetProperty('min_up', $oNode->GetProperty('min_up')); + $oNewNode->SetProperty('threshold', $oNode->GetProperty('threshold')); + $oNewNode->SetProperty('is_reached_count', $iNbReached); $oNewNode->SetProperty('is_reached', true); } } @@ -677,16 +771,24 @@ class DisplayableGraph extends SimpleGraph } } - $iNbGrouping = 1; - //for($iter=0; $iter<$iNbGrouping; $iter++) + $oNodesIter = new RelationTypeIterator($oNewGraph, 'Node'); + foreach($oNodesIter as $oNode) { - $oNodesIter = new RelationTypeIterator($oNewGraph, 'Node'); - foreach($oNodesIter as $oNode) + if ($oNode->GetProperty('source')) { - if ($oNode->GetProperty('source')) - { - $oNode->GroupSimilarNeighbours($oNewGraph, $iGroupingThreshold, true, true); - } + $oNode->GroupSimilarNeighbours($oNewGraph, $iGroupingThreshold, true, true); + } + } + // Groups numbering + $oIterator = new RelationTypeIterator($oNewGraph, 'Node'); + $iGroupIdx = 0; + foreach($oIterator as $oNode) + { + if ($oNode instanceof DisplayableGroupNode) + { + $aGroups[] = $oNode->GetObjects(); + $oNode->SetProperty('group_index', $iGroupIdx); + $iGroupIdx++; } } @@ -811,8 +913,10 @@ class DisplayableGraph extends SimpleGraph /** * Renders as JSON string suitable for loading into the simple_graph widget */ - function GetAsJSON() + function GetAsJSON($sContextKey) { + $aContextDefs = $this->GetContextDefinitions($sContextKey, false); + $aData = array('nodes' => array(), 'edges' => array()); $iGroupIdx = 0; $oIterator = new RelationTypeIterator($this, 'Node'); @@ -824,7 +928,7 @@ class DisplayableGraph extends SimpleGraph $oNode->SetProperty('group_index', $iGroupIdx); $iGroupIdx++; } - $aData['nodes'][] = $oNode->GetForRaphael(); + $aData['nodes'][] = $oNode->GetForRaphael($aContextDefs); } $oIterator = new RelationTypeIterator($this, 'Edge'); @@ -846,13 +950,15 @@ class DisplayableGraph extends SimpleGraph * Renders the graph in a PDF document: centered in the current page * @param PDFPage $oPage The PDFPage representing the PDF document to draw into * @param string $sComments An optional comment to display next to the graph (HTML entities will be escaped, \n replaced by
) + * @param string $sContextKey The key to fetch the queries in the configuration. Example: itop-tickets/relation_context/UserRequest/impacts/down * @param float $xMin Left coordinate of the bounding box to display the graph * @param float $xMax Right coordinate of the bounding box to display the graph * @param float $yMin Top coordinate of the bounding box to display the graph * @param float $yMax Bottom coordinate of the bounding box to display the graph */ - function RenderAsPDF(PDFPage $oPage, $sComments = '', $xMin = -1, $xMax = -1, $yMin = -1, $yMax = -1) + function RenderAsPDF(PDFPage $oPage, $sComments = '', $sContextKey, $xMin = -1, $xMax = -1, $yMin = -1, $yMax = -1) { + $aContextDefs = $this->GetContextDefinitions($sContextKey, false); // No need to develop the parameters $oPdf = $oPage->get_tcpdf(); $aBB = $this->GetBoundingBox(); @@ -904,14 +1010,14 @@ class DisplayableGraph extends SimpleGraph foreach($oIterator as $sId => $oEdge) { set_time_limit($iLoopTimeLimit); - $oEdge->RenderAsPDF($oPdf, $this, $fScale); + $oEdge->RenderAsPDF($oPdf, $this, $fScale, $aContextDefs); } $oIterator = new RelationTypeIterator($this, 'Node'); foreach($oIterator as $sId => $oNode) { set_time_limit($iLoopTimeLimit); - $oNode->RenderAsPDF($oPdf, $this, $fScale); + $oNode->RenderAsPDF($oPdf, $this, $fScale, $aContextDefs); } $oIterator = new RelationTypeIterator($this, 'Node'); $oPdf->SetAutoPageBreak(true, $fBreakMargin); @@ -950,7 +1056,7 @@ class DisplayableGraph extends SimpleGraph $fMaxWidth = max($width, $fMaxWidth); $aClasses[$sClass] = $sClassLabel; $sIconUrl = $oNode->GetProperty('icon_url'); - $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-production/', $sIconUrl); + $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-'.utils::GetCurrentEnvironment().'/', $sIconUrl); $aIcons[$sClass] = $sIconPath; } } @@ -987,6 +1093,40 @@ class DisplayableGraph extends SimpleGraph return array('xmin' => $fMaxWidth + $fIconSize + 4*$fPadding, 'xmax' => $xMax, 'ymin' => $yMin, 'ymax' => $yMax); } + //itop-tickets/relation_context/UserRequest/impacts/down + /** + * + * @param string $sContextKey The key to fetch the queries in the configuration. Example: itop-tickets/relation_context/UserRequest/impacts/down + */ + public function GetContextDefinitions($sContextKey, $bDevelopParams = true, $aContextParams = array()) + { + $aLevels = explode('/', $sContextKey); + $aRelationContext = MetaModel::GetConfig()->GetModuleSetting($aLevels[0], $aLevels[1], array()); + $aContextDefs = array(); + if (isset($aRelationContext[$aLevels[2]][$aLevels[3]][$aLevels[4]]['items'])) + { + $aContextDefs = $aRelationContext[$aLevels[2]][$aLevels[3]][$aLevels[4]]['items']; + + } + + // Check if the queries are valid + foreach($aContextDefs as $sKey => $sDefs) + { + $sOQL = $aContextDefs[$sKey]['oql']; + try + { + // Expand the parameters. If anything goes wrong, then the query is considered as invalid and removed from the list + $oSearch = DBObjectSearch::FromOQL($sOQL); + $aContextDefs[$sKey]['oql'] = $oSearch->ToOQL($bDevelopParams, $aContextParams); + } + catch(Exception $e) + { + unset($aContextDefs[$sKey]); + } + } + return $aContextDefs; + } + /** * Display the graph inside the given page, with the "filter" drawer above it * @param WebPage $oP @@ -995,8 +1135,9 @@ class DisplayableGraph extends SimpleGraph * @param ApplicationContext $oAppContext * @param array $aExcludedObjects */ - function Display(WebPage $oP, $aResults, $sRelation, ApplicationContext $oAppContext, $aExcludedObjects = array(), $sObjClass = null, $iObjKey = null) + function Display(WebPage $oP, $aResults, $sRelation, ApplicationContext $oAppContext, $aExcludedObjects = array(), $sObjClass = null, $iObjKey = null, $sContextKey, $aContextParams = array()) { + $aContextDefs = $this->GetContextDefinitions($sContextKey, true, $aContextParams); $aExcludedByClass = array(); foreach($aExcludedObjects as $oObj) { @@ -1026,7 +1167,7 @@ EOF $aSortedElements[$sSubClass] = MetaModel::GetName($sSubClass); } } - + asort($aSortedElements); $idx = 0; foreach($aSortedElements as $sSubClass => $sClassName) @@ -1038,7 +1179,13 @@ EOF $oP->add("\n"); $oP->add("
\n"); $oP->add("
".Dict::S('UI:ElementsDisplayed')."
\n"); - + + $aAdditionalContexts = array(); + foreach($aContextDefs as $sKey => $aDefinition) + { + $aAdditionalContexts[] = array('key' => $sKey, 'label' => Dict::S($aDefinition['dict']), 'oql' => $aDefinition['oql']); + } + $sDirection = utils::ReadParam('d', 'horizontal'); $iGroupingThreshold = utils::ReadParam('g', 5); @@ -1087,6 +1234,11 @@ EOF 'comments' => Dict::S('UI:RelationOption:Comments'), 'grouping_threshold' => Dict::S('UI:RelationOption:GroupingThreshold'), 'refresh' => Dict::S('UI:Button:Refresh'), + 'check_all' => Dict::S('UI:SearchValue:CheckAll'), + 'uncheck_all' => Dict::S('UI:SearchValue:UncheckAll'), + 'none_selected' => Dict::S('UI:Relation:NoneSelected'), + 'nb_selected' => Dict::S('UI:SearchValue:NbSelected'), + 'additional_context_info' => Dict::S('UI:Relation:AdditionalContextInfo'), ), 'page_format' => array( 'label' => Dict::S('UI:Relation:PDFExportPageFormat'), @@ -1103,16 +1255,17 @@ EOF 'L' => Dict::S('UI:PageOrientation_Landscape'), ), ), + 'additional_contexts' => $aAdditionalContexts, + 'context_key' => $sContextKey, ); - if (!extension_loaded('gd')) + if (!extension_loaded('gd')) { // PDF export requires GD unset($aParams['export_as_pdf']); } if (!extension_loaded('gd') || is_null($sObjClass) || is_null($iObjKey)) { - // PDF export requires GD AND a valid objclass/objkey couple - unset($aParams['export_as_pdf']); + // Export as Attachment requires GD (for building the PDF) AND a valid objclass/objkey couple unset($aParams['export_as_attachment']); } $oP->add_ready_script("$('#$sId').simple_graph(".json_encode($aParams).");"); diff --git a/core/metamodel.class.php b/core/metamodel.class.php index 908b9913f..3b1b1194f 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -1458,13 +1458,17 @@ abstract class MetaModel * * @return RelationGraph The graph of all the related objects */ - static public function GetRelatedObjectsDown($sRelCode, $aSourceObjects, $iMaxDepth = 99, $bEnableRedundancy = true, $aUnreachable = array()) + static public function GetRelatedObjectsDown($sRelCode, $aSourceObjects, $iMaxDepth = 99, $bEnableRedundancy = true, $aUnreachable = array(), $aContexts = array()) { $oGraph = new RelationGraph(); foreach ($aSourceObjects as $oObject) { $oGraph->AddSourceObject($oObject); } + foreach($aContexts as $key => $sOQL) + { + $oGraph->AddContextQuery($key, $sOQL); + } $oGraph->ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy, $aUnreachable); return $oGraph; } @@ -1479,13 +1483,17 @@ abstract class MetaModel * * @return RelationGraph The graph of all the related objects */ - static public function GetRelatedObjectsUp($sRelCode, $aSourceObjects, $iMaxDepth = 99, $bEnableRedundancy = true) + static public function GetRelatedObjectsUp($sRelCode, $aSourceObjects, $iMaxDepth = 99, $bEnableRedundancy = true, $aContexts = array()) { $oGraph = new RelationGraph(); foreach ($aSourceObjects as $oObject) { $oGraph->AddSinkObject($oObject); } + foreach($aContexts as $key => $sOQL) + { + $oGraph->AddContextQuery($key, $sOQL); + } $oGraph->ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy); return $oGraph; } diff --git a/core/relationgraph.class.inc.php b/core/relationgraph.class.inc.php index 5ed1e0326..cbcd72727 100644 --- a/core/relationgraph.class.inc.php +++ b/core/relationgraph.class.inc.php @@ -169,6 +169,7 @@ class RelationGraph extends SimpleGraph protected $aSourceNodes; // Index of source nodes (for a quicker access) protected $aSinkNodes; // Index of sink nodes (for a quicker access) protected $aRedundancySettings; // Cache of user settings + protected $aContextSearches; // Context ("knowing that") stored as a hash array 'class' => DBObjectSearch public function __construct() { @@ -176,6 +177,7 @@ class RelationGraph extends SimpleGraph $this->aSourceNodes = array(); $this->aSinkNodes = array(); $this->aRedundancySettings = array(); + $this->aContextSearches = array(); } /** @@ -197,6 +199,74 @@ class RelationGraph extends SimpleGraph $oSinkNode->SetProperty('sink', true); $this->aSinkNodes[$oSinkNode->GetId()] = $oSinkNode; } + + /** + * Add a 'context' OQL query, specifying extra objects to be marked as 'is_reached' + * even though they are not part of the sources. + * @param string $sOQL The OQL query defining the context objects + */ + public function AddContextQuery($key, $sOQL) + { + if ($sOQL === '') return; + + $oSearch = DBObjectSearch::FromOQL($sOQL); + $aAliases = $oSearch->GetSelectedClasses(); + if (count($aAliases) < 2 ) + { + IssueLog::Error("Invalid context query '$sOQL'. A context query must contain at least two columns."); + throw new Exception("Invalid context query '$sOQL'. A context query must contain at least two columns. Columns: ".implode(', ', $aAliases).'. '); + } + $aAliasNames = array_keys($aAliases); + $sClassAlias = $oSearch->GetClassAlias(); + $oCondition = new BinaryExpression(new FieldExpression('id', $aAliasNames[0]), '=', new VariableExpression('id')); + $oSearch->AddConditionExpression($oCondition); + + $sClass = $oSearch->GetClass(); + if (!array_key_exists($sClass, $this->aContextSearches)) + { + $this->aContextSearches[$sClass] = array(); + } + $this->aContextSearches[$sClass][] = array('key' => $key, 'search' => $oSearch); + } + + /** + * Determines if the given DBObject is part of a 'context' + * @param DBObject $oObj + * @return boolean + */ + public function IsPartOfContext(DBObject $oObj, &$aRootCauses) + { + $bRet = false; + $sFinalClass = get_class($oObj); + $aParentClasses = MetaModel::EnumParentClasses($sFinalClass, ENUM_PARENT_CLASSES_ALL); + + foreach($aParentClasses as $sClass) + { + if (array_key_exists($sClass, $this->aContextSearches)) + { + foreach($this->aContextSearches[$sClass] as $aContextQuery) + { + $aAliases = $aContextQuery['search']->GetSelectedClasses(); + $aAliasNames = array_keys($aAliases); + $sRootCauseAlias = $aAliasNames[1]; // 1st column (=0) = object, second column = root cause + $oSet = new DBObjectSet($aContextQuery['search'], array(), array('id' => $oObj->GetKey())); + while($aRow = $oSet->FetchAssoc()) + { + if (!is_null($aRow[$sRootCauseAlias])) + { + if (!array_key_exists($aContextQuery['key'], $aRootCauses)) + { + $aRootCauses[$aContextQuery['key']] = array(); + } + $aRootCauses[$aContextQuery['key']][] = $aRow[$sRootCauseAlias]; + $bRet = true; + } + } + } + } + } + return $bRet; + } /** * Build the graph downstream, and mark the nodes that can be reached from the source node @@ -220,9 +290,6 @@ class RelationGraph extends SimpleGraph { $oNode->SetProperty('is_reached_allowed', false); } - else - { - } } // Determine the reached nodes @@ -231,6 +298,19 @@ class RelationGraph extends SimpleGraph $oSourceNode->ReachDown('is_reached', true); //echo "
After reaching from {$oSourceNode->GetId()}
\n".$this->DumpAsHtmlImage()."
\n"; } + + // Mark also the "context" nodes as reached and record the "root causes" for each node + $oIterator = new RelationTypeIterator($this, 'Node'); + foreach($oIterator as $oNode) + { + $oObj = $oNode->GetProperty('object'); + $aRootCauses = array(); + if (!is_null($oObj) && $this->IsPartOfContext($oObj, $aRootCauses)) + { + $oNode->SetProperty('context_root_causes', $aRootCauses); + $oNode->ReachDown('is_reached', true); + } + } } /** @@ -245,6 +325,19 @@ class RelationGraph extends SimpleGraph $this->AddRelatedObjects($sRelCode, false, $oSinkNode, $iMaxDepth, $bEnableRedundancy); //echo "
After processing of {$oSinkNode->GetId()}
\n".$this->DumpAsHtmlImage()."
\n"; } + + // Mark also the "context" nodes as reached and record the "root causes" for each node + $oIterator = new RelationTypeIterator($this, 'Node'); + foreach($oIterator as $oNode) + { + $oObj = $oNode->GetProperty('object'); + $aRootCauses = array(); + if (!is_null($oObj) && $this->IsPartOfContext($oObj, $aRootCauses)) + { + $oNode->SetProperty('context_root_causes', $aRootCauses); + $oNode->ReachDown('is_reached', true); + } + } } diff --git a/datamodels/2.x/itop-change-mgmt-itil/datamodel.itop-change-mgmt-itil.xml b/datamodels/2.x/itop-change-mgmt-itil/datamodel.itop-change-mgmt-itil.xml index 78e8d0369..ec86df4a9 100755 --- a/datamodels/2.x/itop-change-mgmt-itil/datamodel.itop-change-mgmt-itil.xml +++ b/datamodels/2.x/itop-change-mgmt-itil/datamodel.itop-change-mgmt-itil.xml @@ -4516,4 +4516,64 @@ fast + + + + + + + + + id)]]> + Tickets:Related:OpenChanges + itop-change-mgmt/images/change-ongoing.png + + + id) AND (DATE_ADD(C.end_date, INTERVAL 3 DAY) < NOW())]]> + Tickets:Related:RecentChanges + itop-change-mgmt/images/change-done.png + + + + + + + + + + + + + + + + Tickets:Related:OpenChanges + itop-change-mgmt/images/change-ongoing.png + + + + Tickets:Related:RecentChanges + itop-change-mgmt/images/change-done.png + + + + + + + id)]]> + Tickets:Related:OpenChanges + itop-change-mgmt/images/change-ongoing.png + + + id) AND (DATE_ADD(C.end_date, INTERVAL 3 DAY) < NOW())]]> + Tickets:Related:RecentChanges + itop-change-mgmt/images/change-done.png + + + + + + + + diff --git a/datamodels/2.x/itop-change-mgmt-itil/images/change-done.png b/datamodels/2.x/itop-change-mgmt-itil/images/change-done.png new file mode 100755 index 000000000..e27142f66 Binary files /dev/null and b/datamodels/2.x/itop-change-mgmt-itil/images/change-done.png differ diff --git a/datamodels/2.x/itop-change-mgmt-itil/images/change-ongoing.png b/datamodels/2.x/itop-change-mgmt-itil/images/change-ongoing.png new file mode 100755 index 000000000..0cc46b863 Binary files /dev/null and b/datamodels/2.x/itop-change-mgmt-itil/images/change-ongoing.png differ diff --git a/datamodels/2.x/itop-change-mgmt/datamodel.itop-change-mgmt.xml b/datamodels/2.x/itop-change-mgmt/datamodel.itop-change-mgmt.xml index 48cc23167..d845533f9 100755 --- a/datamodels/2.x/itop-change-mgmt/datamodel.itop-change-mgmt.xml +++ b/datamodels/2.x/itop-change-mgmt/datamodel.itop-change-mgmt.xml @@ -817,4 +817,64 @@ fast + + + + + + + + + id)]]> + Tickets:Related:OpenChanges + itop-change-mgmt/images/change-ongoing.png + + + id) AND (DATE_ADD(C.end_date, INTERVAL 3 DAY) < NOW())]]> + Tickets:Related:RecentChanges + itop-change-mgmt/images/change-done.png + + + + + + + + + + + + + + + + Tickets:Related:OpenChanges + itop-change-mgmt/images/change-ongoing.png + + + + Tickets:Related:RecentChanges + itop-change-mgmt/images/change-done.png + + + + + + + id)]]> + Tickets:Related:OpenChanges + itop-change-mgmt/images/change-ongoing.png + + + id) AND (DATE_ADD(C.end_date, INTERVAL 3 DAY) < NOW())]]> + Tickets:Related:RecentChanges + itop-change-mgmt/images/change-done.png + + + + + + + + diff --git a/datamodels/2.x/itop-change-mgmt/de.dict.itop-change-mgmt.php b/datamodels/2.x/itop-change-mgmt/de.dict.itop-change-mgmt.php index 8a4e2d6a9..0d7da474a 100644 --- a/datamodels/2.x/itop-change-mgmt/de.dict.itop-change-mgmt.php +++ b/datamodels/2.x/itop-change-mgmt/de.dict.itop-change-mgmt.php @@ -102,6 +102,8 @@ Dict::Add('DE DE', 'German', 'Deutsch', array( 'UI-ChangeManagementOverview-Last-7-days' => 'Zahl der Changes in den letzten sieben Tagen', 'UI-ChangeManagementOverview-ChangeByDomain-last-7-days' => 'Changes der letzten sieben Tage nach Typ', 'UI-ChangeManagementOverview-ChangeByStatus-last-7-days' => 'Changes der letzten sieben Tage nach Status', + 'Tickets:Related:OpenChanges' => 'Open changes~~', + 'Tickets:Related:RecentChanges' => 'Recent changes~~', 'Class:Change/Attribute:changemanager_email' => 'Change Manager Email', 'Class:Change/Attribute:changemanager_email+' => '', 'Class:Change/Attribute:parent_name' => 'Parent Change ref', diff --git a/datamodels/2.x/itop-change-mgmt/en.dict.itop-change-mgmt.php b/datamodels/2.x/itop-change-mgmt/en.dict.itop-change-mgmt.php index b70ed3803..76e5bb7b0 100755 --- a/datamodels/2.x/itop-change-mgmt/en.dict.itop-change-mgmt.php +++ b/datamodels/2.x/itop-change-mgmt/en.dict.itop-change-mgmt.php @@ -46,6 +46,8 @@ Dict::Add('EN US', 'English', 'English', array( 'UI-ChangeManagementOverview-Last-7-days' => 'Number of changes for the last 7 days', 'UI-ChangeManagementOverview-ChangeByDomain-last-7-days' => 'Changes by domain for the last 7 days', 'UI-ChangeManagementOverview-ChangeByStatus-last-7-days' => 'Changes by status for the last 7 days', + 'Tickets:Related:OpenChanges' => 'Open changes', + 'Tickets:Related:RecentChanges' => 'Recent changes', )); // Dictionnay conventions diff --git a/datamodels/2.x/itop-change-mgmt/fr.dict.itop-change-mgmt.php b/datamodels/2.x/itop-change-mgmt/fr.dict.itop-change-mgmt.php index 1f4c75e77..6d5c13d7d 100755 --- a/datamodels/2.x/itop-change-mgmt/fr.dict.itop-change-mgmt.php +++ b/datamodels/2.x/itop-change-mgmt/fr.dict.itop-change-mgmt.php @@ -125,8 +125,7 @@ Dict::Add('FR FR', 'French', 'Français', array( 'UI-ChangeManagementOverview-ChangeByDomain-last-7-days' => 'Changements par domaine', 'UI-ChangeManagementOverview-ChangeByStatus-last-7-days' => 'Changements par statut', 'UI:ChangeMgmtMenuOverview:Title' => 'Tableau de bord des changements pour les 7 derniers jours', - - - + 'Tickets:Related:OpenChanges' => 'Changements en cours', + 'Tickets:Related:RecentChanges' => 'Changements récents', )); ?> diff --git a/datamodels/2.x/itop-change-mgmt/images/change-done.png b/datamodels/2.x/itop-change-mgmt/images/change-done.png new file mode 100755 index 000000000..e27142f66 Binary files /dev/null and b/datamodels/2.x/itop-change-mgmt/images/change-done.png differ diff --git a/datamodels/2.x/itop-change-mgmt/images/change-ongoing.png b/datamodels/2.x/itop-change-mgmt/images/change-ongoing.png new file mode 100755 index 000000000..0cc46b863 Binary files /dev/null and b/datamodels/2.x/itop-change-mgmt/images/change-ongoing.png differ diff --git a/datamodels/2.x/itop-incident-mgmt-itil/datamodel.itop-incident-mgmt-itil.xml b/datamodels/2.x/itop-incident-mgmt-itil/datamodel.itop-incident-mgmt-itil.xml index d768ceac6..e323aae8a 100755 --- a/datamodels/2.x/itop-incident-mgmt-itil/datamodel.itop-incident-mgmt-itil.xml +++ b/datamodels/2.x/itop-incident-mgmt-itil/datamodel.itop-incident-mgmt-itil.xml @@ -1761,4 +1761,49 @@ + + + + + + + + + id)]]> + Tickets:Related:OpenIncidents + itop-request-mgmt/images/incident-red.png + + + + + + + + + + + + + + + id)]]> + Tickets:Related:OpenIncidents + itop-request-mgmt/images/incident-red.png + + + + + + + id)]]> + Tickets:Related:OpenIncidents + itop-request-mgmt/images/incident-red.png + + + + + + + + diff --git a/datamodels/2.x/itop-incident-mgmt-itil/images/incident-red.png b/datamodels/2.x/itop-incident-mgmt-itil/images/incident-red.png new file mode 100755 index 000000000..5aac4ab4b Binary files /dev/null and b/datamodels/2.x/itop-incident-mgmt-itil/images/incident-red.png differ diff --git a/datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml b/datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml index 52241a102..2df925ca0 100755 --- a/datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml +++ b/datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml @@ -1844,4 +1844,49 @@ fast + + + + + + + + + id)]]> + Tickets:Related:OpenIncidents + itop-request-mgmt/images/incident-red.png + + + + + + + + + + + + + + + id)]]> + Tickets:Related:OpenIncidents + itop-request-mgmt/images/incident-red.png + + + + + + + id)]]> + Tickets:Related:OpenIncidents + itop-request-mgmt/images/incident-red.png + + + + + + + + diff --git a/datamodels/2.x/itop-request-mgmt/de.dict.itop-request-mgmt.php b/datamodels/2.x/itop-request-mgmt/de.dict.itop-request-mgmt.php index 8708341a1..76115dfa8 100644 --- a/datamodels/2.x/itop-request-mgmt/de.dict.itop-request-mgmt.php +++ b/datamodels/2.x/itop-request-mgmt/de.dict.itop-request-mgmt.php @@ -268,5 +268,6 @@ Dict::Add('DE DE', 'German', 'Deutsch', array( 'Portal:SelectLanguage' => 'Ändern Sie Ihre Spracheinstellung', 'Portal:LanguageChangedTo_Lang' => 'Spracheinstellung geändert auf: ', 'Portal:ChooseYourFavoriteLanguage' => 'WÄhlen Sie Ihre bevorzugte Sprache', + 'Tickets:Related:OpenIncidents' => 'Open incidents~~', )); ?> diff --git a/datamodels/2.x/itop-request-mgmt/en.dict.itop-request-mgmt.php b/datamodels/2.x/itop-request-mgmt/en.dict.itop-request-mgmt.php index 3e6db35f4..e66d35365 100755 --- a/datamodels/2.x/itop-request-mgmt/en.dict.itop-request-mgmt.php +++ b/datamodels/2.x/itop-request-mgmt/en.dict.itop-request-mgmt.php @@ -58,6 +58,7 @@ Dict::Add('EN US', 'English', 'English', array( 'Menu:UserRequest:MyWorkOrders' => 'Work orders assigned to me', 'Menu:UserRequest:MyWorkOrders+' => 'All work orders assigned to me', 'Class:Problem:KnownProblemList' => 'Known problems', + 'Tickets:Related:OpenIncidents' => 'Open incidents', )); // Dictionnay conventions diff --git a/datamodels/2.x/itop-request-mgmt/fr.dict.itop-request-mgmt.php b/datamodels/2.x/itop-request-mgmt/fr.dict.itop-request-mgmt.php index 5e3592f9d..8699783d8 100755 --- a/datamodels/2.x/itop-request-mgmt/fr.dict.itop-request-mgmt.php +++ b/datamodels/2.x/itop-request-mgmt/fr.dict.itop-request-mgmt.php @@ -215,6 +215,7 @@ Dict::Add('FR FR', 'French', 'Français', array( 'Class:UserRequest/Stimulus:ev_wait_for_approval' => 'Attendre une approbation', 'Class:UserRequest/Stimulus:ev_wait_for_approval+' => '', 'Class:UserRequest/Error:CannotAssignParentRequestIdToSelf' => 'La Requête parente ne peut pas être assignée à elle même', + 'Tickets:Related:OpenIncidents' => 'Incidents en cours', )); diff --git a/datamodels/2.x/itop-request-mgmt/images/incident-red.png b/datamodels/2.x/itop-request-mgmt/images/incident-red.png new file mode 100755 index 000000000..5aac4ab4b Binary files /dev/null and b/datamodels/2.x/itop-request-mgmt/images/incident-red.png differ diff --git a/dictionaries/de.dictionary.itop.ui.php b/dictionaries/de.dictionary.itop.ui.php index 6e3e9bdd9..89c19f113 100644 --- a/dictionaries/de.dictionary.itop.ui.php +++ b/dictionaries/de.dictionary.itop.ui.php @@ -778,6 +778,8 @@ Wenn Aktionen mit Trigger verknüpft sind, bekommt jede Aktion eine Auftragsnumm 'UI:RelationGroupNumber_N' => 'Gruppe #%1$d~~', 'UI:Relation:ExportAsPDF' => 'Export as PDF...~~', 'UI:RelationOption:GroupingThreshold' => 'Grouping threshold~~', + 'UI:Relation:AdditionalContextInfo' => 'Additional context info~~', + 'UI:Relation:NoneSelected' => 'Nichts~~', 'UI:Relation:ExportAsDocument' => 'Export as Document...~~', 'UI:Relation:DrillDown' => 'Details...~~', 'UI:Relation:PDFExportOptions' => 'PDF Export Options~~', diff --git a/dictionaries/dictionary.itop.ui.php b/dictionaries/dictionary.itop.ui.php index 1be80e823..93d04ace8 100644 --- a/dictionaries/dictionary.itop.ui.php +++ b/dictionaries/dictionary.itop.ui.php @@ -971,6 +971,8 @@ When associated with a trigger, each action is given an "order" number, specifyi 'UI:RelationGroupNumber_N' => 'Group #%1$d', 'UI:Relation:ExportAsPDF' => 'Export as PDF...', 'UI:RelationOption:GroupingThreshold' => 'Grouping threshold', + 'UI:Relation:AdditionalContextInfo' => 'Additional context info', + 'UI:Relation:NoneSelected' => 'None', 'UI:Relation:ExportAsAttachment' => 'Export as Attachment...', 'UI:Relation:DrillDown' => 'Details...', 'UI:Relation:PDFExportOptions' => 'PDF Export Options', diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php index 4a7d1ac3e..e9e048748 100644 --- a/dictionaries/fr.dictionary.itop.ui.php +++ b/dictionaries/fr.dictionary.itop.ui.php @@ -814,6 +814,8 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé 'UI:RelationGroupNumber_N' => 'Groupe n°%1$d', 'UI:Relation:ExportAsPDF' => 'Exporter en PDF...', 'UI:RelationOption:GroupingThreshold' => 'Seuil de groupage', + 'UI:Relation:AdditionalContextInfo' => 'Infos complémentaires de contexte', + 'UI:Relation:NoneSelected' => 'Aucune', 'UI:Relation:ExportAsAttachment' => 'Exporter comme une Pièce Jointe...', 'UI:Relation:DrillDown' => 'Détails...', 'UI:Relation:PDFExportOptions' => 'Options de l\'export en PDF', diff --git a/js/simple_graph.js b/js/simple_graph.js index 3f4589d8c..4602902ef 100644 --- a/js/simple_graph.js +++ b/js/simple_graph.js @@ -28,14 +28,21 @@ $(function() include_list: 'Include the list of objects', comments: 'Comments', grouping_threshold: 'Grouping Threshold', - refresh: 'Refresh' + additional_context_info: 'Additional Context Info', + refresh: 'Refresh', + check_all: 'Check All', + uncheck_all: 'Uncheck All', + none_selected: 'None', + nb_selected: '# selected', }, export_as_document: null, drill_down: null, grouping_threshold: 10, excluded_classes: [], attachment_obj_class: null, - attachment_obj_key: null + attachment_obj_key: null, + additional_contexts: [], + context_key: '' }, // the constructor @@ -107,6 +114,7 @@ $(function() this.aEdges[k].aElements = []; this._draw_edge(this.aEdges[k]); } + this._make_tooltips(); }, _draw_node: function(oNode) { @@ -158,6 +166,19 @@ $(function() oNode.aElements.push(this.oPaper.image(oNode.icon_url, xPos - iWidth * this.fZoom/2, yPos - iHeight * this.fZoom/2, iWidth*this.fZoom, iHeight*this.fZoom).colorShift('#fff', 1)); } oNode.aElements.push(this.oPaper.image(oNode.icon_url, xPos - iWidth * this.fZoom/2, yPos - iHeight * this.fZoom/2, iWidth*this.fZoom, iHeight*this.fZoom).attr(oNode.icon_attr)); + + var idx = 0; + for(var i in oNode.context_icons) + { + var sgn = 2*(idx % 2) -1; // Suite: -1, 1, -1, 1, -1, 1, -1, etc. + var coef = Math.floor((1+idx)/2) * sgn; // Suite: 0, 1, -1, 2, -2, 3, -3, etc. + var alpha = coef*Math.PI/4 - Math.PI/2; + var x = xPos + Math.cos(alpha) * 1.25*iWidth * this.fZoom / 2; + var y = yPos + Math.sin(alpha) * 1.25*iWidth * this.fZoom / 2; + var l = iWidth/3 * this.fZoom; + oNode.aElements.push(this.oPaper.image(oNode.context_icons[i], x - l/2, y - l/2, l , l).attr(oNode.icon_attr)); + idx++; + } var oText = this.oPaper.text( xPos, yPos, oNode.label); oNode.text_attr['font-size'] = iFontSize * this.fZoom; oText.attr(oNode.text_attr); @@ -186,7 +207,18 @@ $(function() { var sNodeId = oNode.id; $(oNode.aElements[k].node).attr({'data-type': oNode.shape, 'data-id': oNode.id} ).attr('class', 'popupMenuTarget'); - oNode.aElements[k].drag(function(dx, dy, x, y, event) { me._move(sNodeId, dx, dy, x, y, event); }, function(x, y, event) { me._drag_start(sNodeId, x, y, event); }, function (event) { me._drag_end(sNodeId, event); }); + oNode.aElements[k].drag( + function(dx, dy, x, y, event) { + clearTimeout($(this.node).data('openTimeoutId')); + me._move(sNodeId, dx, dy, x, y, event); + }, + function(x, y, event) { + me._drag_start(sNodeId, x, y, event); + }, + function (event) { + me._drag_end(sNodeId, event); + } + ); } }, _move: function(sNodeId, dx, dy, x, y, event) @@ -367,7 +399,17 @@ $(function() var sPopupMenuId = 'tk_graph'+this.element.attr('id'); var sHtml = '
'; var sId = this.element.attr('id'); - sHtml += this.options.labels.grouping_threshold+'  '; + sHtml += this.options.labels.grouping_threshold+' '; + if (this.options.additional_contexts.length > 0) + { + sHtml += ' '+this.options.labels.additional_context_info+' ' + } + sHtml += ' '; sHtml += '