浏览代码

Relations & Impact analysis enhancements:
- Detailled tooltips in the graph
- Context queries ("knowing that")

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3583 a333f486-631f-4898-b8df-5754b55c2be0

dflaven 10 年之前
父节点
当前提交
bf800862db
共有 26 个文件被更改,包括 688 次插入71 次删除
  1. 188 35
      core/displayablegraph.class.inc.php
  2. 10 2
      core/metamodel.class.php
  3. 96 3
      core/relationgraph.class.inc.php
  4. 60 0
      datamodels/2.x/itop-change-mgmt-itil/datamodel.itop-change-mgmt-itil.xml
  5. 二进制
      datamodels/2.x/itop-change-mgmt-itil/images/change-done.png
  6. 二进制
      datamodels/2.x/itop-change-mgmt-itil/images/change-ongoing.png
  7. 60 0
      datamodels/2.x/itop-change-mgmt/datamodel.itop-change-mgmt.xml
  8. 2 0
      datamodels/2.x/itop-change-mgmt/de.dict.itop-change-mgmt.php
  9. 2 0
      datamodels/2.x/itop-change-mgmt/en.dict.itop-change-mgmt.php
  10. 2 3
      datamodels/2.x/itop-change-mgmt/fr.dict.itop-change-mgmt.php
  11. 二进制
      datamodels/2.x/itop-change-mgmt/images/change-done.png
  12. 二进制
      datamodels/2.x/itop-change-mgmt/images/change-ongoing.png
  13. 45 0
      datamodels/2.x/itop-incident-mgmt-itil/datamodel.itop-incident-mgmt-itil.xml
  14. 二进制
      datamodels/2.x/itop-incident-mgmt-itil/images/incident-red.png
  15. 45 0
      datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml
  16. 1 0
      datamodels/2.x/itop-request-mgmt/de.dict.itop-request-mgmt.php
  17. 1 0
      datamodels/2.x/itop-request-mgmt/en.dict.itop-request-mgmt.php
  18. 1 0
      datamodels/2.x/itop-request-mgmt/fr.dict.itop-request-mgmt.php
  19. 二进制
      datamodels/2.x/itop-request-mgmt/images/incident-red.png
  20. 2 0
      dictionaries/de.dictionary.itop.ui.php
  21. 2 0
      dictionaries/dictionary.itop.ui.php
  22. 2 0
      dictionaries/fr.dictionary.itop.ui.php
  23. 131 5
      js/simple_graph.js
  24. 21 2
      pages/UI.php
  25. 15 11
      pages/ajax.render.php
  26. 2 10
      setup/modelfactory.class.inc.php

+ 188 - 35
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()."<hr/>";
+		$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 .= '<p><img style="max-height: 24px; vertical-align:bottom;" src="'.utils::GetAbsoluteUrlModulesRoot().$aContext['icon'].'" title="'.htmlentities(Dict::S($aContext['dict'])).'">&nbsp;'.implode(', ', $aRootCauses).'</p>';
+			}
+			$sHtml .= '<hr/>';
+		}
+		$sHtml .= '<table><tbody>';
+		foreach(MetaModel::GetZListItems($sSubClass, 'list') as $sAttCode)
+		{
+			$oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode);
+			$sHtml .= '<tr><td>'.$oAttDef->GetLabel().':&nbsp;</td><td>'.$oCurrObj->GetAsHtml($sAttCode).'</td></tr>';
+		}
+		$sHtml .= '</tbody></table>';
+		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<hr>";
+		$sHtml .= '<table><tbody>';
+		$sHtml .= "<tr><td># Items Impacted:&nbsp;</td><td>".$this->GetProperty('is_reached_count')."&nbsp;/&nbsp;".($this->GetProperty('min_up') + $this->GetProperty('threshold'))."</td></tr>";
+		$sHtml .= "<tr><td>Critical Threshold:&nbsp;</td><td>".$this->GetProperty('threshold')."&nbsp;/&nbsp;".($this->GetProperty('min_up') + $this->GetProperty('threshold'))."</td></tr>";
+		$sHtml .= '</tbody></table>';
+		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 <br/>)
+	 * @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("</div>\n");
 		$oP->add("<div class=\"HRDrawer\"></div>\n");
 		$oP->add("<div id=\"dh_flash\" class=\"DrawerHandle\">".Dict::S('UI:ElementsDisplayed')."</div>\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).");");

+ 10 - 2
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;
 	}

+ 96 - 3
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 "<h5>After reaching from {$oSourceNode->GetId()}</h5>\n".$this->DumpAsHtmlImage()."<br/>\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 "<h5>After processing of {$oSinkNode->GetId()}</h5>\n".$this->DumpAsHtmlImage()."<br/>\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);
+			}	
+		}
 	}
 
 

+ 60 - 0
datamodels/2.x/itop-change-mgmt-itil/datamodel.itop-change-mgmt-itil.xml

@@ -4516,4 +4516,64 @@
       <auto_reload>fast</auto_reload>
     </menu>
   </menus>
+  <module_parameters>
+    <parameters id="itop-tickets">
+    <relation_context>
+      <UserRequest>
+        <impacts>
+        <down>
+          <items type="array">
+             <item id="open_changes" _delta="define">
+              <oql><![CDATA[SELECT FCI, C FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN Change AS C ON L.ticket_id = C.id WHERE (C.status NOT IN ('closed', 'rejected')) AND (L.impact_code != 'not_impacted') AND (C.id != :this->id)]]></oql>
+              <dict>Tickets:Related:OpenChanges</dict>
+              <icon>itop-change-mgmt/images/change-ongoing.png</icon>
+            </item>
+             <item id="recent_changes" _delta="define">
+              <oql><![CDATA[SELECT FCI, C FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN Change AS C ON L.ticket_id = C.id WHERE (C.status NOT IN ('closed')) AND (L.impact_code != 'not_impacted') AND (C.id != :this->id) AND (DATE_ADD(C.end_date, INTERVAL 3 DAY) < NOW())]]></oql>
+              <dict>Tickets:Related:RecentChanges</dict>
+              <icon>itop-change-mgmt/images/change-done.png</icon>
+            </item>
+          </items>
+        </down>
+      </impacts>
+      </UserRequest>
+    </relation_context>
+    </parameters>
+    <parameters id="itop-config-mgmt">
+    <relation_context>
+      <FunctionalCI>
+        <impacts>
+          <down>
+            <items type="array">
+               <item id="open_changes" _delta="define">
+                <oql><![CDATA[SELECT FCI, C FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN Change AS C ON L.ticket_id = C.id WHERE (C.status NOT IN ('closed', 'rejected')) AND (L.impact_code != 'not_impacted')]]></oql>
+                <dict>Tickets:Related:OpenChanges</dict>
+                <icon>itop-change-mgmt/images/change-ongoing.png</icon>
+              </item>
+               <item id="recent_changes" _delta="define">
+                <oql><![CDATA[SELECT FCI, C FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN Change AS C ON L.ticket_id = C.id WHERE (C.status NOT IN ('closed')) AND (L.impact_code != 'not_impacted') AND (DATE_ADD(C.end_date, INTERVAL 3 DAY) < NOW())]]></oql>
+                <dict>Tickets:Related:RecentChanges</dict>
+                <icon>itop-change-mgmt/images/change-done.png</icon>
+              </item>
+            </items>
+          </down>
+          <up>
+            <items type="array">
+               <item id="open_changes" _delta="define">
+                <oql><![CDATA[SELECT FCI, C FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN Change AS C ON L.ticket_id = C.id WHERE (C.status NOT IN ('closed', 'rejected')) AND (L.impact_code != 'not_impacted') AND (C.id != :this->id)]]></oql>
+                <dict>Tickets:Related:OpenChanges</dict>
+                <icon>itop-change-mgmt/images/change-ongoing.png</icon>
+              </item>
+               <item id="recent_changes" _delta="define">
+                <oql><![CDATA[SELECT FCI, C FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN Change AS C ON L.ticket_id = C.id WHERE (C.status NOT IN ('closed')) AND (L.impact_code != 'not_impacted') AND (C.id != :this->id) AND (DATE_ADD(C.end_date, INTERVAL 3 DAY) < NOW())]]></oql>
+                <dict>Tickets:Related:RecentChanges</dict>
+                <icon>itop-change-mgmt/images/change-done.png</icon>
+              </item>
+            </items>
+          </up>
+        </impacts>
+      </FunctionalCI>
+    </relation_context>
+    </parameters>
+  </module_parameters>
 </itop_design>

二进制
datamodels/2.x/itop-change-mgmt-itil/images/change-done.png


二进制
datamodels/2.x/itop-change-mgmt-itil/images/change-ongoing.png


+ 60 - 0
datamodels/2.x/itop-change-mgmt/datamodel.itop-change-mgmt.xml

@@ -817,4 +817,64 @@
       <auto_reload>fast</auto_reload>
     </menu>
   </menus>
+  <module_parameters>
+    <parameters id="itop-tickets">
+    <relation_context>
+      <UserRequest>
+        <impacts>
+          <down>
+            <items type="array">
+               <item id="open_changes" _delta="define">
+                <oql><![CDATA[SELECT FCI, C FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN Change AS C ON L.ticket_id = C.id WHERE (C.status NOT IN ('closed', 'rejected')) AND (L.impact_code != 'not_impacted') AND (C.id != :this->id)]]></oql>
+                <dict>Tickets:Related:OpenChanges</dict>
+                <icon>itop-change-mgmt/images/change-ongoing.png</icon>
+              </item>
+               <item id="recent_changes" _delta="define">
+                <oql><![CDATA[SELECT FCI, C FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN Change AS C ON L.ticket_id = C.id WHERE (C.status NOT IN ('closed')) AND (L.impact_code != 'not_impacted') AND (C.id != :this->id) AND (DATE_ADD(C.end_date, INTERVAL 3 DAY) < NOW())]]></oql>
+                <dict>Tickets:Related:RecentChanges</dict>
+                <icon>itop-change-mgmt/images/change-done.png</icon>
+              </item>
+            </items>
+          </down>
+        </impacts>
+      </UserRequest>
+    </relation_context>
+    </parameters>
+    <parameters id="itop-config-mgmt">
+    <relation_context>
+      <FunctionalCI>
+        <impacts>
+          <down>
+            <items type="array">
+               <item id="open_changes" _delta="define">
+                <oql><![CDATA[SELECT FCI, C FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN Change AS C ON L.ticket_id = C.id WHERE (C.status NOT IN ('closed', 'rejected')) AND (L.impact_code != 'not_impacted')]]></oql>
+                <dict>Tickets:Related:OpenChanges</dict>
+                <icon>itop-change-mgmt/images/change-ongoing.png</icon>
+              </item>
+               <item id="recent_changes" _delta="define">
+                <oql><![CDATA[SELECT FCI, C FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN Change AS C ON L.ticket_id = C.id WHERE (C.status NOT IN ('closed')) AND (L.impact_code != 'not_impacted') AND (DATE_ADD(C.end_date, INTERVAL 3 DAY) < NOW())]]></oql>
+                <dict>Tickets:Related:RecentChanges</dict>
+                <icon>itop-change-mgmt/images/change-done.png</icon>
+              </item>
+            </items>
+          </down>
+          <up>
+            <items type="array">
+               <item id="open_changes" _delta="define">
+                <oql><![CDATA[SELECT FCI, C FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN Change AS C ON L.ticket_id = C.id WHERE (C.status NOT IN ('closed', 'rejected')) AND (L.impact_code != 'not_impacted') AND (C.id != :this->id)]]></oql>
+                <dict>Tickets:Related:OpenChanges</dict>
+                <icon>itop-change-mgmt/images/change-ongoing.png</icon>
+              </item>
+               <item id="recent_changes" _delta="define">
+                <oql><![CDATA[SELECT FCI, C FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN Change AS C ON L.ticket_id = C.id WHERE (C.status NOT IN ('closed')) AND (L.impact_code != 'not_impacted') AND (C.id != :this->id) AND (DATE_ADD(C.end_date, INTERVAL 3 DAY) < NOW())]]></oql>
+                <dict>Tickets:Related:RecentChanges</dict>
+                <icon>itop-change-mgmt/images/change-done.png</icon>
+              </item>
+            </items>
+          </up>
+        </impacts>
+      </FunctionalCI>
+    </relation_context>
+    </parameters>
+  </module_parameters>
 </itop_design>

+ 2 - 0
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',

+ 2 - 0
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

+ 2 - 3
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',
 ));
 ?>

二进制
datamodels/2.x/itop-change-mgmt/images/change-done.png


二进制
datamodels/2.x/itop-change-mgmt/images/change-ongoing.png


+ 45 - 0
datamodels/2.x/itop-incident-mgmt-itil/datamodel.itop-incident-mgmt-itil.xml

@@ -1761,4 +1761,49 @@
       </profile>
     </profiles>
   </user_rights>
+  <module_parameters>
+    <parameters id="itop-tickets">
+      <relation_context>
+        <Incident>
+          <impacts>
+            <down>
+              <items type="array">
+                <item id="open_incidents" _delta="define">
+                  <oql><![CDATA[SELECT FCI, I FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN Incident AS I ON L.ticket_id = I.id WHERE (I.status NOT IN ('closed', 'resolved')) AND (L.impact_code != 'not_impacted') AND (I.id != :this->id)]]></oql>
+                  <dict>Tickets:Related:OpenIncidents</dict>
+                  <icon>itop-request-mgmt/images/incident-red.png</icon>
+                </item>
+              </items>
+            </down>
+          </impacts>
+        </Incident>
+      </relation_context>
+    </parameters>
+    <parameters id="itop-config-mgmt">
+      <relation_context>
+        <FunctionalCI>
+          <impacts>
+            <down>
+              <items type="array">
+                <item id="open_incidents" _delta="define">
+                  <oql><![CDATA[SELECT FCI, I FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN Incident AS I ON L.ticket_id = I.id WHERE (I.status NOT IN ('closed', 'resolved')) AND (L.impact_code != 'not_impacted') AND (I.id != :this->id)]]></oql>
+                  <dict>Tickets:Related:OpenIncidents</dict>
+                  <icon>itop-request-mgmt/images/incident-red.png</icon>
+                </item>
+              </items>
+            </down>
+            <up>
+              <items type="array">
+                <item id="open_incidents" _delta="define">
+                  <oql><![CDATA[SELECT FCI, I FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN Incident AS I ON L.ticket_id = I.id WHERE (I.status NOT IN ('closed', 'resolved')) AND (L.impact_code != 'not_impacted') AND (I.id != :this->id)]]></oql>
+                  <dict>Tickets:Related:OpenIncidents</dict>
+                  <icon>itop-request-mgmt/images/incident-red.png</icon>
+                </item>
+              </items>
+            </up>
+          </impacts>
+        </FunctionalCI>
+      </relation_context>
+    </parameters>
+  </module_parameters>
 </itop_design>

二进制
datamodels/2.x/itop-incident-mgmt-itil/images/incident-red.png


+ 45 - 0
datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml

@@ -1844,4 +1844,49 @@
       <auto_reload>fast</auto_reload>
     </menu>
   </menus>
+  <module_parameters>
+    <parameters id="itop-tickets">
+      <relation_context>
+        <UserRequest>
+          <impacts>
+            <down>
+              <items type="array">
+                <item id="open_incidents" _delta="define">
+                  <oql><![CDATA[SELECT FCI, R FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN UserRequest AS R ON L.ticket_id = R.id WHERE (R.status NOT IN ('closed', 'resolved')) AND (R.request_type='incident') AND (L.impact_code != 'not_impacted') AND (R.id != :this->id)]]></oql>
+                  <dict>Tickets:Related:OpenIncidents</dict>
+                  <icon>itop-request-mgmt/images/incident-red.png</icon>
+                </item>
+              </items>
+            </down>
+          </impacts>
+        </UserRequest>
+      </relation_context>
+    </parameters>
+    <parameters id="itop-config-mgmt">
+      <relation_context>
+        <FunctionalCI>
+          <impacts>
+            <down>
+              <items type="array">
+                <item id="open_incidents" _delta="define">
+                  <oql><![CDATA[SELECT FCI, R FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN UserRequest AS R ON L.ticket_id = R.id WHERE (R.status NOT IN ('closed', 'resolved')) AND (R.request_type='incident') AND (L.impact_code != 'not_impacted') AND (R.id != :this->id)]]></oql>
+                  <dict>Tickets:Related:OpenIncidents</dict>
+                  <icon>itop-request-mgmt/images/incident-red.png</icon>
+                </item>
+              </items>
+            </down>
+            <up>
+              <items type="array">
+                <item id="open_incidents" _delta="define">
+                  <oql><![CDATA[SELECT FCI, R FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN UserRequest AS R ON L.ticket_id = R.id WHERE (R.status NOT IN ('closed', 'resolved')) AND (R.request_type='incident') AND (L.impact_code != 'not_impacted') AND (R.id != :this->id)]]></oql>
+                  <dict>Tickets:Related:OpenIncidents</dict>
+                  <icon>itop-request-mgmt/images/incident-red.png</icon>
+                </item>
+              </items>
+            </up>
+          </impacts>
+        </FunctionalCI>
+      </relation_context>
+    </parameters>
+  </module_parameters>
 </itop_design>

+ 1 - 0
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~~',
 ));
 ?>

+ 1 - 0
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

+ 1 - 0
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',
 ));
 
 

二进制
datamodels/2.x/itop-request-mgmt/images/incident-red.png


+ 2 - 0
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~~',

+ 2 - 0
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',

+ 2 - 0
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',

+ 131 - 5
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 = '<div class="graph_config">';
 			var sId = this.element.attr('id');
-			sHtml += this.options.labels.grouping_threshold+'&nbsp;<input type="text" name="g" value="'+this.options.grouping_threshold+'" id="'+sId+'_grouping_threshold" size="2">&nbsp;<button type="button" id="'+sId+'_refresh_btn">'+this.options.labels.refresh+'</button>';
+			sHtml += this.options.labels.grouping_threshold+'&nbsp;<input type="text" name="g" value="'+this.options.grouping_threshold+'" id="'+sId+'_grouping_threshold" size="2">';
+			if (this.options.additional_contexts.length > 0)
+			{
+				sHtml += '&nbsp;'+this.options.labels.additional_context_info+' <select id="'+sId+'_contexts" name="contexts" class="multiselect" multiple size="1">';
+				for(var k in this.options.additional_contexts)
+				{
+					sHtml += '<option value="'+k+'" selected>'+this.options.additional_contexts[k].label+'</option>';
+				}
+				sHtml += '</select>'
+			}
+			sHtml += '&nbsp;<button type="button" id="'+sId+'_refresh_btn">'+this.options.labels.refresh+'</button>';
 			sHtml += '<div class="itop_popup toolkit_menu graph" style="font-size: 12px;" id="'+sPopupMenuId+'"><ul><li><img src="../images/toolkit_menu.png"><ul>';
 			if (this.options.export_as_pdf != null)
 			{
@@ -389,6 +431,7 @@ $(function()
 			$('#'+sPopupMenuId+'_pdf').click(function() { me.export_as_pdf(); });
 			$('#'+sPopupMenuId+'_attachment').click(function() { me.export_as_attachment(); });
 			$('#'+sId+'_grouping_threshold').spinner({ min: 2});
+			$('#'+sId+'_contexts').multiselect({header: true, checkAllText: this.options.labels.check_all, uncheckAllText: this.options.labels.uncheck_all, noneSelectedText: this.options.labels.none_selected, selectedText: this.options.labels.nb_selected, selectedList: 1});
 			$('#'+sId+'_refresh_btn').button().click(function() { me.reload(); });
 		},
 		_build_context_menus: function()
@@ -456,6 +499,8 @@ $(function()
 		},
 		_export_dlg: function(sTitle, sSubmitUrl, sOperation)
 		{
+			var sId = this.element.attr('id');
+			var me = this;
 			var oPositions = {};
 			for(k in this.aNodes)
 			{
@@ -463,6 +508,11 @@ $(function()
 			}
 			var sHtmlForm = '<div id="GraphExportDlg'+this.element.attr('id')+'"><form id="graph_'+this.element.attr('id')+'_export_dlg" target="_blank" action="'+sSubmitUrl+'" method="post">';
 			sHtmlForm += '<input type="hidden" name="g" value="'+this.options.grouping_threshold+'">';
+			sHtmlForm += '<input type="hidden" name="context_key" value="'+this.options.context_key+'">';
+			$('#'+sId+'_contexts').multiselect('getChecked').each(function() {
+				sHtmlForm += '<input type="hidden" name="contexts['+$(this).val()+']" value="'+me.options.additional_contexts[$(this).val()].oql+'">';				
+			});
+
 			sHtmlForm += '<input type="hidden" name="positions" value="">';
 			for(k in this.options.excluded_classes)
 			{
@@ -561,9 +611,12 @@ $(function()
 				this.options.grouping_threshold = 2;
 				$('#'+sId+'_grouping_threshold').val(this.options.grouping_threshold);
 			}
+			var aContexts = [];
+			$('#'+sId+'_contexts').multiselect('getChecked').each(function() { aContexts[$(this).val()] = me.options.additional_contexts[$(this).val()].oql; });
 			this.element.closest('.ui-tabs').tabs({ heightStyle: "fill" });
+			this._close_all_tooltips();
 			this.oPaper.rect(0, 0, this.element.width(), this.element.height()).attr({fill: '#000', opacity: 0.4, 'stroke-width': 0});
-			$.post(sUrl, {excluded_classes: this.options.excluded_classes, g: this.options.grouping_threshold, sources: this.options.sources, excluded: this.options.excluded }, function(data) {
+			$.post(sUrl, {excluded_classes: this.options.excluded_classes, g: this.options.grouping_threshold, sources: this.options.sources, excluded: this.options.excluded, contexts: aContexts, context_key: this.options.context_key }, function(data) {
 				me.load(data);
 			}, 'json');
 		},
@@ -619,6 +672,79 @@ $(function()
 		reload: function()
 		{
 			this.load_from_url(this.options.load_from_url);
+		},
+		_make_tooltips: function()
+		{
+			var me  = this;
+			$( ".popupMenuTarget" ).tooltip({
+				content: function() {
+					var sDataId = $(this).attr('data-id');
+					var sTooltipContent = '<div class="tooltip-close-button" data-id="'+sDataId+'" style="display:inline-block; float:right; cursor:pointer; padding-left:0.25em;">×</div>';
+					sTooltipContent += me._get_tooltip_content(sDataId);
+					return sTooltipContent;
+				},
+				items: '.popupMenuTarget',
+				position: {
+					my: "center bottom-10",
+					at: "center  top",					
+					using: function( position, feedback ) { 
+						$(this).css( position );  
+						$( "<div>" )
+						.addClass( "arrow" )
+						.addClass( feedback.vertical )
+						.addClass( feedback.horizontal )
+						.appendTo( this );
+						}
+				}
+			})
+			.off( "mouseover mouseout" )
+			.on( "mouseover", function(event){
+				event.stopImmediatePropagation();
+				var jMe = $(this);
+				$(this).data('openTimeoutId', setTimeout(function() {
+					var sDataId = jMe.attr('data-id');
+					if ($('.tooltip-close-button[data-id="'+sDataId+'"]').length == 0)
+					{
+						jMe.tooltip('open');						
+					}
+				}, 500));					
+			})
+			.on( "mouseout", function(event){
+				event.stopImmediatePropagation();
+				clearTimeout($(this).data('openTimeoutId'));					
+			});
+			/* Happens at every on_drag_end !!!
+			.on( "click", function(){
+				var sDataId = $(this).attr('data-id');
+				if ($('.tooltip-close-button[data-id="'+sDataId+'"]').length == 0)
+				{
+					$(this).tooltip( 'open' );							 
+				}
+				else
+				{
+					$(this).tooltip( 'close' );							 						
+				}           
+				$( this ).unbind( "mouseleave" );
+				return false;	
+			 });
+			*/
+			$('body').on('click', '.tooltip-close-button', function() {
+				var sDataId = $(this).attr('data-id');
+				$('.popupMenuTarget[data-id="'+sDataId+'"]').tooltip('close');
+			});
+		},
+		_get_tooltip_content: function(sNodeId)
+		{
+			var oNode = this._find_node(sNodeId);
+			if (oNode !== null)
+			{
+				return oNode.tooltip;
+			}
+			return '<p>Node Id:'+sNodeId+'</p>';
+		},
+		_close_all_tooltips: function()
+		{
+			this.element.find('.popupMenuTarget').tooltip('close');
 		}
 	});	
 });

+ 21 - 2
pages/UI.php

@@ -1439,6 +1439,7 @@ EOF
 		$iGroupingThreshold = utils::ReadParam('g', 5);
 		
 		$oObj = MetaModel::GetObject($sClass, $id);
+		$sRootClass = MetaModel::GetRootClass($sClass);
 		$iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
 		$aSourceObjects = array($oObj);
 		if ($sRelation == 'depends on')
@@ -1476,17 +1477,35 @@ EOF
 		$oP->SetCurrentTabContainer('Navigator');
 		
 		$sFirstTab = MetaModel::GetConfig()->Get('impact_analysis_first_tab');
+		$sContextKey = "itop-config-mgmt/relation_context/$sRootClass/$sRelation/$sDirection";
+		
+		// Check if the current object supports Attachments, similar to AttachmentPlugin::IsTargetObject
+		$sClassForAttachment = null;
+		$iIdForAttachment = null;
+		if (class_exists('Attachment'))
+		{
+			$aAllowedClasses = MetaModel::GetModuleSetting('itop-attachments', 'allowed_classes', array('Ticket'));
+			foreach($aAllowedClasses as $sAllowedClass)
+			{
+				if ($oObj instanceof $sAllowedClass)
+				{
+					$iIdForAttachment = $id;
+					$sClassForAttachment = $sClass;
+				}
+			}
+		}
+		// Display the tabs
 		if ($sFirstTab == 'list')
 		{
 			DisplayNavigatorListTab($oP, $aResults, $sRelation, $oObj);
 			$oP->SetCurrentTab(Dict::S('UI:RelationshipGraph'));
-			$oDisplayGraph->Display($oP, $aResults, $sRelation, $oAppContext);
+			$oDisplayGraph->Display($oP, $aResults, $sRelation, $oAppContext, array(), $sClassForAttachment, $iIdForAttachment, $sContextKey, array('this' => $oObj));
 			DisplayNavigatorGroupTab($oP, $aGroups, $sRelation, $oObj);
 		}
 		else
 		{
 			$oP->SetCurrentTab(Dict::S('UI:RelationshipGraph'));
-			$oDisplayGraph->Display($oP, $aResults, $sRelation, $oAppContext);
+			$oDisplayGraph->Display($oP, $aResults, $sRelation, $oAppContext, array(), $sClassForAttachment, $iIdForAttachment, $sContextKey, array('this' => $oObj));
 			DisplayNavigatorListTab($oP, $aResults, $sRelation, $oObj);
 			DisplayNavigatorGroupTab($oP, $aGroups, $sRelation, $oObj);
 		}

+ 15 - 11
pages/ajax.render.php

@@ -1743,6 +1743,8 @@ EOF
 		$aExcludedClasses = utils::ReadParam('excluded_classes', array(), false, 'raw_data');
 		$bIncludeList = (bool)utils::ReadParam('include_list', false);
 		$sComments = utils::ReadParam('comments', '', false, 'raw_data');
+		$aContexts = utils::ReadParam('contexts', array(), false, 'raw_data');
+		$sContextKey = utils::ReadParam('context_key', array(), false, 'raw_data');
 		$aPositions = null;
 		if ($sPositions != null)
 		{
@@ -1780,11 +1782,11 @@ EOF
 		$iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
 		if ($sDirection == 'up')
 		{
-			$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth);
+			$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aContexts);
 		}
 		else
 		{
-			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects);
+			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects, $aContexts);
 		}
 		
 		// Remove excluded classes from the graph
@@ -1809,20 +1811,18 @@ EOF
 		{
 			$oGraph->UpdatePositions($aPositions);
 		}
-		$iGroupIdx = 0;
+
 		$aGroups = array();
 		$oIterator = new RelationTypeIterator($oGraph, 'Node');
 		foreach($oIterator as $oNode)
 		{
 			if ($oNode instanceof DisplayableGroupNode)
 			{
-				$aGroups[] = $oNode->GetObjects();
-				$oNode->SetProperty('group_index', $iGroupIdx);
-				$iGroupIdx++;
+				$aGroups[$oNode->GetProperty('group_index')] = $oNode->GetObjects();
 			}
 		}
 		// First page is the graph
-		$oGraph->RenderAsPDF($oPage, $sComments);
+		$oGraph->RenderAsPDF($oPage, $sComments, $sContextKey);
 
 		if ($bIncludeList)
 		{
@@ -1908,6 +1908,8 @@ EOF
 		$iGroupingThreshold = utils::ReadParam('g', 5);
 		$sPositions = utils::ReadParam('positions', null, false, 'raw_data');
 		$aExcludedClasses = utils::ReadParam('excluded_classes', array(), false, 'raw_data');
+		$aContexts = utils::ReadParam('contexts', array(), false, 'raw_data');
+		$sContextKey = utils::ReadParam('context_key', array(), false, 'raw_data');
 		$aPositions = null;
 		if ($sPositions != null)
 		{
@@ -1946,11 +1948,11 @@ EOF
 		$iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
 		if ($sDirection == 'up')
 		{
-			$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth);
+			$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aContexts);
 		}
 		else
 		{
-			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects);
+			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects, $aContexts);
 		}
 		
 		// Remove excluded classes from the graph
@@ -1973,7 +1975,7 @@ EOF
 		{
 			$oGraph->UpdatePositions($aPositions);
 		}
-		$oPage->add($oGraph->GetAsJSON());
+		$oPage->add($oGraph->GetAsJSON($sContextKey));
 		$oPage->SetContentType('application/json');
 		break;
 		
@@ -2024,8 +2026,10 @@ EOF
 		
 		$aResults = $oRelGraph->GetObjectsByClass();
 		$oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down'));
+		
+		$sContextKey = 'itop-tickets/relation_context/'.$sClass.'/'.$sRelation.'/'.$sDirection;		
 		$oAppContext = new ApplicationContext();
-		$oGraph->Display($oPage, $aResults, $sRelation, $oAppContext, $aExcludedObjects, $sClass, $iId);		
+		$oGraph->Display($oPage, $aResults, $sRelation, $oAppContext, $aExcludedObjects, $sClass, $iId, $sContextKey, array('this' => $oTicket));		
 		break;
 		
 		default:

+ 2 - 10
setup/modelfactory.class.inc.php

@@ -2152,16 +2152,8 @@ class MFParameters
 								throw new Exception("Invalid Parameters: mixed tags ('$sFirstTagName' and '".$oChildElement->nodeName."') inside array '".$oNode->nodeName."'");
 							}
 							$val = $this->ReadElement($oChildElement);
-							$idx = (string)$oChildElement->getAttribute('id'); // Don't cast into float, since floats are converted to int (i.e. truncated) when used as hash indexes (cf: http://php.net/manual/en/language.types.array.php)
-							if ($idx !== '')
-							{
-								$value[$idx] = $val;
-							}
-							else
-							{
-								// No specific Id, just push the value at the end of the array
-								$value[] = $val;
-							}
+							// No specific Id, just push the value at the end of the array
+							$value[] = $val;
 						}
 					}
 					ksort($value, SORT_NUMERIC);