Browse Source

Integration of the new impact analysis into the tickets.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3578 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 10 năm trước cách đây
mục cha
commit
76a43b3264

+ 22 - 19
application/itopwebpage.class.inc.php

@@ -218,7 +218,7 @@ EOF;
 			},
 			beforeLoad: function( event, ui ) {
 				if ( ui.tab.data('loaded') && (ui.tab.attr('data-cache') == 'true')) {
-					event.defaultPrevented = true;
+					event.preventDefault();
 					return;
 				}
 				ui.panel.html('<div><img src="../images/indicator.gif"></div>');
@@ -867,26 +867,29 @@ EOF
 		}
 		else if ($this->GetOutputFormat() == 'pdf' && $this->IsOutputFormatAvailable('pdf') )
 		{
-			require_once(APPROOT.'lib/MPDF/mpdf.php');
-			$oMPDF = new mPDF('c');
-			$oMPDF->mirroMargins = false;
-			if ($this->a_base['href'] != '')
+			if (@is_readable(APPROOT.'lib/MPDF/mpdf.php'))
 			{
-					$oMPDF->setBasePath($this->a_base['href']); // Seems that the <BASE> tag is not recognized by mPDF...
-			}
-			$oMPDF->showWatermarkText = true;
-			if ($this->GetOutputOption('pdf', 'template_path'))
-			{
-					$oMPDF->setImportUse(); // Allow templates
-					$oMPDF->SetDocTemplate ($this->GetOutputOption('pdf', 'template_path'), 1);
-			}
-			$oMPDF->WriteHTML($sHtml);
-			$sOutputName = $this->s_title.'.pdf';
-			if ($this->GetOutputOption('pdf', 'output_name'))
-			{
-				$sOutputName = $this->GetOutputOption('pdf', 'output_name');
+				require_once(APPROOT.'lib/MPDF/mpdf.php');
+				$oMPDF = new mPDF('c');
+				$oMPDF->mirroMargins = false;
+				if ($this->a_base['href'] != '')
+				{
+						$oMPDF->setBasePath($this->a_base['href']); // Seems that the <BASE> tag is not recognized by mPDF...
+				}
+				$oMPDF->showWatermarkText = true;
+				if ($this->GetOutputOption('pdf', 'template_path'))
+				{
+						$oMPDF->setImportUse(); // Allow templates
+						$oMPDF->SetDocTemplate ($this->GetOutputOption('pdf', 'template_path'), 1);
+				}
+				$oMPDF->WriteHTML($sHtml);
+				$sOutputName = $this->s_title.'.pdf';
+				if ($this->GetOutputOption('pdf', 'output_name'))
+				{
+					$sOutputName = $this->GetOutputOption('pdf', 'output_name');
+				}
+				$oMPDF->Output($sOutputName, 'I');
 			}
-			$oMPDF->Output($sOutputName, 'I');
 		}
 		MetaModel::RecordQueryTrace();
 		ExecutionKPI::ReportStats();

+ 2 - 2
application/pdfpage.class.inc.php

@@ -29,8 +29,8 @@ class iTopPDF extends TCPDF
 		$aMargins = $this->getMargins();
 		
 		// Display the title (centered)
-		$this->SetY(0);
-		$this->MultiCell($this->getPageWidth() - $aMargins['left'] - $aMargins['right'] - $iPageNumberWidth, 15, $this->sDocumentTitle, 0, 'C', false, 0 /* $ln */, '', '', true, 0, false, true, 15, 'M' /* $valign */);
+		$this->SetXY($aMargins['left'] + $iPageNumberWidth, 0);
+		$this->MultiCell($this->getPageWidth() - $aMargins['left'] - $aMargins['right'] - 2*$iPageNumberWidth, 15, $this->sDocumentTitle, 0, 'C', false, 0 /* $ln */, '', '', true, 0, false, true, 15, 'M' /* $valign */);
 		$this->SetFont('dejavusans', '', 10);
 		
 		// Display the page number (right aligned)

+ 192 - 85
core/displayablegraph.class.inc.php

@@ -560,13 +560,17 @@ class DisplayableGroupNode extends DisplayableNode
  */
 class DisplayableGraph extends SimpleGraph
 {
-	protected $sDirection;
+	protected $bDirectionDown;
 	protected $aTempImages;
+	protected $aSourceObjects;
+	protected $aSinkObjects;
 	
 	public function __construct()
 	{
 		parent::__construct();
 		$this->aTempImages = array();
+		$this->aSourceObjects = array();
+		$this->aSinkObjects = array();
 	}
 	
 	public function GetTempImageName()
@@ -594,6 +598,7 @@ class DisplayableGraph extends SimpleGraph
 	public static function FromRelationGraph(RelationGraph $oGraph, $iGroupingThreshold = 20, $bDirectionDown = true)
 	{
 		$oNewGraph = new DisplayableGraph();
+		$oNewGraph->bDirectionDown = $bDirectionDown;
 		
 		$oNodesIter = new RelationTypeIterator($oGraph, 'Node');
 		foreach($oNodesIter as $oNode)
@@ -603,16 +608,27 @@ class DisplayableGraph extends SimpleGraph
 				case 'RelationObjectNode':				
 				$oNewNode = new DisplayableNode($oNewGraph, $oNode->GetId(), 0, 0);
 				
+				$oObj = $oNode->GetProperty('object');
+				$sClass = get_class($oObj);
 				if ($oNode->GetProperty('source'))
 				{
+					if (!array_key_exists($sClass, $oNewGraph->aSourceObjects))
+					{
+						$oNewGraph->aSourceObjects[$sClass] = array();
+					}
+					$oNewGraph->aSourceObjects[$sClass][] = $oObj->GetKey();
 					$oNewNode->SetProperty('source', true);
 				}
 				if ($oNode->GetProperty('sink'))
 				{
+					if (!array_key_exists($sClass, $oNewGraph->aSinkObjects))
+					{
+						$oNewGraph->aSinkObjects[$sClass] = array();
+					}
+					$oNewGraph->aSinkObjects[$sClass][] = $oObj->GetKey();
 					$oNewNode->SetProperty('sink', true);
 				}
-				$oObj = $oNode->GetProperty('object');
-				$oNewNode->SetProperty('class', get_class($oObj));
+				$oNewNode->SetProperty('class', $sClass);
 				$oNewNode->SetProperty('object', $oObj);
 				$oNewNode->SetProperty('icon_url', $oObj->GetIcon(false));
 				$oNewNode->SetProperty('label', $oObj->GetRawName());
@@ -791,71 +807,6 @@ class DisplayableGraph extends SimpleGraph
 			}
 		}
 	}
-	
-	/**
-	 * Renders as a suite of Javascript instructions to display the graph using the simple_graph widget
-	 * @param WebPage $oP
-	 * @param string $sId
-	 * @param string $sExportAsPdfURL
-	 * @param string $sExportAsDocumentURL
-	 * @param string $sDrillDownURL
-	 */
-	function RenderAsRaphael(WebPage $oP, $sId = null, $sExportAsPdfURL, $sExportAsDocumentURL, $sDrillDownURL)
-	{
-		if ($sId == null)
-		{
-			$sId = 'graph';
-		}
-		$oP->add('<div id="'.$sId.'" class="simple-graph"></div>');
-		$aParams = array(
-			'export_as_pdf' => array('url' => $sExportAsPdfURL, 'label' => Dict::S('UI:Relation:ExportAsPDF')),
-			'export_as_document' => array('url' => $sExportAsDocumentURL, 'label' => Dict::S('UI:Relation:ExportAsDocument')),
-			'drill_down' => array('url' => $sDrillDownURL, 'label' => Dict::S('UI:Relation:DrillDown')),
-			'labels' => array(
-				'export_pdf_title' => Dict::S('UI:Relation:PDFExportOptions'),
-				'export' => Dict::S('UI:Relation:PDFExportOptions'),
-				'cancel' => Dict::S('UI:Button:Cancel'),
-			),
-			'page_format' => array(
-				'label' => Dict::S('UI:Relation:PDFExportPageFormat'),
-				'values' => array(
-					'A3' => Dict::S('UI:PageFormat_A3'),
-					'A4' => Dict::S('UI:PageFormat_A4'),
-					'Letter' => Dict::S('UI:PageFormat_Letter'),
-				),
-			),
-			'page_orientation' => array(
-				'label' => Dict::S('UI:Relation:PDFExportPageOrientation'),
-				'values' => array(
-					'P' => Dict::S('UI:PageOrientation_Portrait'),
-					'L' => Dict::S('UI:PageOrientation_Landscape'),
-				),
-			),
-		);
-		$oP->add_ready_script("var oGraph = $('#$sId').simple_graph(".json_encode($aParams).");");
-		
-		$oIterator = new RelationTypeIterator($this, 'Node');
-		foreach($oIterator as $sId => $oNode)
-		{
-			$aNode = $oNode->GetForRaphael();
-			$sJSNode = json_encode($aNode);
-			$oP->add_ready_script("oGraph.simple_graph('add_node', $sJSNode);");
-		}
-		$oIterator = new RelationTypeIterator($this, 'Edge');
-		foreach($oIterator as $sId => $oEdge)
-		{
-			$aEdge = array();
-			$aEdge['id'] = $oEdge->GetId();
-			$aEdge['source_node_id'] = $oEdge->GetSourceNode()->GetId();
-			$aEdge['sink_node_id'] = $oEdge->GetSinkNode()->GetId();
-			$fOpacity = ($oEdge->GetSinkNode()->GetProperty('is_reached') && $oEdge->GetSourceNode()->GetProperty('is_reached') ? 1 : 0.2);
-			$aEdge['attr'] = array('opacity' => $fOpacity, 'stroke' => '#000');
-			$sJSEdge = json_encode($aEdge);
-			$oP->add_ready_script("oGraph.simple_graph('add_edge', $sJSEdge);");
-		}
-		
-		$oP->add_ready_script("oGraph.simple_graph('draw');");
-	}
 
 	/**
 	 * Renders as JSON string suitable for loading into the simple_graph widget
@@ -964,13 +915,26 @@ class DisplayableGraph extends SimpleGraph
 		$oPdf->SetAlpha(1);
 	}
 	
+	/**
+	 * Renders (in PDF) the key (legend) of the graphics vertically to the left of the specified zone (xmin,ymin, xmax,ymax). Returns the width used by the legend.
+	 * @param TCPDF $oPdf
+	 * @param string $sComments
+	 * @param float $xMin
+	 * @param float $yMin
+	 * @param float $xMax
+	 * @param float $yMax
+	 * @return number The width used by the legend
+	 */
 	protected function RenderKey(TCPDF $oPdf, $sComments, $xMin, $yMin, $xMax, $yMax)
 	{
+		$fFontSize = 7; // in mm
+		$fIconSize = 6; // in mm
+		$fPadding = 1;	// in mm
 		$oIterator = new RelationTypeIterator($this, 'Node');
-		$fMaxWidth = max($oPdf->GetStringWidth(Dict::S('UI:Relation:Key')) - 6, $oPdf->GetStringWidth(Dict::S('UI:Relation:Comments')) - 6);
+		$fMaxWidth = max($oPdf->GetStringWidth(Dict::S('UI:Relation:Key')) - $fIconSize, $oPdf->GetStringWidth(Dict::S('UI:Relation:Comments')) - $fIconSize);
 		$aClasses = array();
 		$aIcons = array();
-		$oPdf->SetFont('dejavusans', '', 8, '', true);
+		$oPdf->SetFont('dejavusans', '', $fFontSize, '', true);
 		foreach($oIterator as $sId => $oNode)
 		{
 			if ($sClass = $oNode->GetProperty('class'))
@@ -987,26 +951,169 @@ class DisplayableGraph extends SimpleGraph
 				}
 			}
 		}
-		$oPdf->SetXY($xMin + 1, $yMin +1);
-		$yPos = $yMin + 1;
+		$oPdf->SetXY($xMin + $fPadding, $yMin + $fPadding);
+		$yPos = $yMin + $fPadding;
 		$oPdf->SetFillColor(225, 225, 225);
-		$oPdf->Cell(7 + $fMaxWidth, 7, Dict::S('UI:Relation:Key'), 0 /* border */, 1 /* ln */, 'C', true /* fill */);
-		$yPos += 8;
+		$oPdf->Cell($fIconSize + $fPadding + $fMaxWidth, $fIconSize + $fPadding, Dict::S('UI:Relation:Key'), 0 /* border */, 1 /* ln */, 'C', true /* fill */);
+		$yPos += $fIconSize + 2*$fPadding;
 		foreach($aClasses as $sClass => $sLabel)
 		{
-			$oPdf->SetX($xMin + 7);
-			$oPdf->Cell(0, 8, $sLabel, 0 /* border */, 1 /* ln */);
-			$oPdf->Image($aIcons[$sClass], $xMin+1, $yPos, 6, 6);
-			$yPos += 8; 
+			$oPdf->SetX($xMin + $fIconSize + $fPadding);
+			$oPdf->Cell(0, $fIconSize + 2*$fPadding, $sLabel, 0 /* border */, 1 /* ln */);
+			$oPdf->Image($aIcons[$sClass], $xMin+1, $yPos, $fIconSize, $fIconSize);
+			$yPos += $fIconSize + 2*$fPadding; 
 		}
-		$oPdf->Rect($xMin, $yMin, $fMaxWidth+9, $yPos - $yMin, 'D');
-		$oPdf->Rect($xMin, $yPos, $fMaxWidth+9, $yMax - $yPos, 'D');
+		$oPdf->Rect($xMin, $yMin, $fMaxWidth + $fIconSize + 3*$fPadding, $yPos - $yMin, 'D');
+		$oPdf->Rect($xMin, $yPos, $fMaxWidth + $fIconSize + 3*$fPadding, $yMax - $yPos, 'D');
 		$yPos +=1;
-		$oPdf->SetXY($xMin + 1, $yPos);
-		$oPdf->Cell(7 + $fMaxWidth, 7, Dict::S('UI:Relation:Comments'), 0 /* border */, 1 /* ln */, 'C', true /* fill */);
-		$yPos += 8;
+		$oPdf->SetXY($xMin + $fPadding, $yPos);
+		$oPdf->Cell($fIconSize + $fPadding + $fMaxWidth, $fIconSize + $fPadding, Dict::S('UI:Relation:Comments'), 0 /* border */, 1 /* ln */, 'C', true /* fill */);
+		$yPos += $fIconSize + 2*$fPadding;
 		$oPdf->SetX($xMin);
-		$oPdf->writeHTMLCell(8 + $fMaxWidth, $yMax - $yPos, $xMin, $yPos, '<p>'.str_replace("\n", '<br/>', htmlentities($sComments, ENT_QUOTES, 'UTF-8')).'</p>', 0 /* border */, 1 /* ln */);
-		return $fMaxWidth + 10;
+		$oPdf->writeHTMLCell($fIconSize + 2*$fPadding + $fMaxWidth, $yMax - $yPos, $xMin, $yPos, '<p>'.str_replace("\n", '<br/>', htmlentities($sComments, ENT_QUOTES, 'UTF-8')).'</p>', 0 /* border */, 1 /* ln */);
+		return $fMaxWidth + $fIconSize + 4*$fPadding;
 	}
+	
+	/**
+	 * Display the graph inside the given page, with the "filter" drawer above it
+	 * @param WebPage $oP
+	 * @param hash $aResults
+	 * @param string $sRelation
+	 * @param ApplicationContext $oAppContext
+	 * @param array $aExcludedObjects
+	 */
+	function Display(WebPage $oP, $aResults, $sRelation, ApplicationContext $oAppContext, $aExcludedObjects)
+	{	
+		$aExcludedByClass = array();
+		foreach($aExcludedObjects as $oObj)
+		{
+			if (!array_key_exists(get_class($oObj), $aExcludedByClass))
+			{
+				$aExcludedByClass[get_class($oObj)] = array();
+			}
+			$aExcludedByClass[get_class($oObj)][] = $oObj->GetKey();
+		}
+		$oP->add("<div id=\"ds_flash\" class=\"SearchDrawer\" style=\"display:none;\">\n");
+		$oP->add_ready_script(
+<<<EOF
+	$( "#tabbedContent_0" ).tabs({ heightStyle: "fill" });
+	$("#dh_flash").click( function() {
+		$("#ds_flash").slideToggle('normal', function() { $("#ds_flash").parent().resize(); } );
+		$("#dh_flash").toggleClass('open');
+	});
+    $('#ReloadMovieBtn').button().button('disable');
+EOF
+		);
+		$aSortedElements = array();
+		foreach($aResults as $sClassIdx => $aObjects)
+		{
+			foreach($aObjects as $oCurrObj)
+			{
+				$sSubClass = get_class($oCurrObj);
+				$aSortedElements[$sSubClass] = MetaModel::GetName($sSubClass);
+			}
+		}
+	
+		asort($aSortedElements);
+		$idx = 0;
+		foreach($aSortedElements as $sSubClass => $sClassName)
+		{
+			$oP->add("<span style=\"padding-right:2em; white-space:nowrap;\"><input type=\"checkbox\" id=\"exclude_$idx\" name=\"excluded[]\" value=\"$sSubClass\" checked onChange=\"$('#ReloadMovieBtn').button('enable')\"><label for=\"exclude_$idx\">&nbsp;".MetaModel::GetClassIcon($sSubClass)."&nbsp;$sClassName</label></span> ");
+			$idx++;
+		}
+		$oP->add("<p style=\"text-align:right\"><button type=\"button\" id=\"ReloadMovieBtn\" onClick=\"DoReload()\">".Dict::S('UI:Button:Refresh')."</button></p>");
+		$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");
+	
+		$sDirection = utils::ReadParam('d', 'horizontal');
+		$iGroupingThreshold = utils::ReadParam('g', 5);
+	
+		$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/fraphael.js');
+		$oP->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/jquery.contextMenu.css');
+		$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.contextMenu.js');
+		$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/simple_graph.js');
+		try
+		{
+			$this->InitFromGraphviz();
+			$sExportAsPdfURL = '';
+			if (extension_loaded('gd'))
+			{
+				$sExportAsPdfURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_pdf&relation='.$sRelation.'&direction='.($this->bDirectionDown ? 'down' : 'up');
+			}
+			$oAppcontext = new ApplicationContext();
+			$sContext = $oAppContext->GetForLink();
+			$sDrillDownURL = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=details&class=%1$s&id=%2$s&'.$sContext;
+			$sExportAsDocumentURL = '';
+			$sLoadFromURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_json&relation='.$sRelation.'&direction='.($this->bDirectionDown ? 'down' : 'up');
+	
+			$sId = 'graph';
+			$oP->add('<div id="'.$sId.'" class="simple-graph"></div>');
+			$aParams = array(
+				'source_url' => $sLoadFromURL,
+				'sources' => ($this->bDirectionDown ? $this->aSourceObjects : $this->aSinkObjects),
+				'excluded' => $aExcludedByClass,
+				'grouping_threshold' => $iGroupingThreshold,
+				'export_as_pdf' => array('url' => $sExportAsPdfURL, 'label' => Dict::S('UI:Relation:ExportAsPDF')),
+				//'export_as_document' => array('url' => $sExportAsDocumentURL, 'label' => Dict::S('UI:Relation:ExportAsDocument')),
+				'drill_down' => array('url' => $sDrillDownURL, 'label' => Dict::S('UI:Relation:DrillDown')),
+				'labels' => array(
+					'export_pdf_title' => Dict::S('UI:Relation:PDFExportOptions'),
+					'export' => Dict::S('UI:Button:Export'),
+					'cancel' => Dict::S('UI:Button:Cancel'),
+					'title' => Dict::S('UI:RelationOption:Title'),
+					'include_list' => Dict::S('UI:RelationOption:IncludeList'),
+					'comments' => Dict::S('UI:RelationOption:Comments'),
+					'grouping_threshold' => Dict::S('UI:RelationOption:GroupingThreshold'),
+					'refresh' => Dict::S('UI:Button:Refresh'),
+				),
+				'page_format' => array(
+					'label' => Dict::S('UI:Relation:PDFExportPageFormat'),
+					'values' => array(
+						'A3' => Dict::S('UI:PageFormat_A3'),
+						'A4' => Dict::S('UI:PageFormat_A4'),
+						'Letter' => Dict::S('UI:PageFormat_Letter'),
+					),
+				),
+				'page_orientation' => array(
+					'label' => Dict::S('UI:Relation:PDFExportPageOrientation'),
+					'values' => array(
+						'P' => Dict::S('UI:PageOrientation_Portrait'),
+						'L' => Dict::S('UI:PageOrientation_Landscape'),
+					),
+				),
+			);
+			$oP->add_ready_script("$('#$sId').simple_graph(".json_encode($aParams).");");
+		}
+		catch(Exception $e)
+		{
+			$oP->add('<div>'.$e->getMessage().'</div>');
+		}
+		$oP->add_script(
+<<<EOF
+		
+	function DoReload()
+	{
+		$('#ReloadMovieBtn').button('disable');
+		try
+		{
+			var aExcluded = [];
+			$('input[name^=excluded]').each( function() {
+				if (!$(this).prop('checked'))
+				{
+					aExcluded.push($(this).val());
+				}
+			} );
+			$('#graph').simple_graph('option', {excluded_classes: aExcluded});
+			$('#graph').simple_graph('reload');
+		}
+		catch(err)
+		{
+			alert(err);
+		}
+	}
+EOF
+		);
+	}
+	
 }

+ 3 - 2
core/metamodel.class.php

@@ -1454,17 +1454,18 @@ abstract class MetaModel
 	 * @param array $asourceObjects The objects to start with
 	 * @param int $iMaxDepth
 	 * @param boolean $bEnableReduncancy
+	 * @param array $aUnreachable Array of objects to be considered as 'unreachable'
 	 * 
 	 * @return RelationGraph The graph of all the related objects
 	 */
-	static public function GetRelatedObjectsDown($sRelCode, $aSourceObjects, $iMaxDepth = 99, $bEnableRedundancy = true)
+	static public function GetRelatedObjectsDown($sRelCode, $aSourceObjects, $iMaxDepth = 99, $bEnableRedundancy = true, $aUnreachable = array())
 	{
 		$oGraph = new RelationGraph();
 		foreach ($aSourceObjects as $oObject)
 		{
 			$oGraph->AddSourceObject($oObject);
 		}
-		$oGraph->ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy);
+		$oGraph->ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy, $aUnreachable);
 		return $oGraph;
 	}
 

+ 42 - 3
core/relationgraph.class.inc.php

@@ -75,11 +75,11 @@ class RelationObjectNode extends GraphNode
 	}
 
 	/**
-	 * Recursively mark the objects nodes as reached, unless we get stopped by a redundancy node
+	 * Recursively mark the objects nodes as reached, unless we get stopped by a redundancy node or a 'not allowed' node
 	 */	 	
 	public function ReachDown($sProperty, $value)
 	{
-		if (is_null($this->GetProperty($sProperty)))
+		if (is_null($this->GetProperty($sProperty)) && ($this->GetProperty($sProperty.'_allowed') !== false))
 		{
 			$this->SetProperty($sProperty, $value);
 			foreach ($this->GetOutgoingEdges() as $oOutgoingEdge)
@@ -201,7 +201,7 @@ class RelationGraph extends SimpleGraph
 	/**
 	 * Build the graph downstream, and mark the nodes that can be reached from the source node
 	 */	 	
-	public function ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy)
+	public function ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy, $aUnreachableObjects = array())
 	{
 		//echo "<h5>Sources only...</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
 		// Build the graph out of the sources
@@ -210,6 +210,21 @@ class RelationGraph extends SimpleGraph
 			$this->AddRelatedObjects($sRelCode, true, $oSourceNode, $iMaxDepth, $bEnableRedundancy);
 			//echo "<h5>After processing of {$oSourceNode->GetId()}</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
 		}
+		
+		// Mark the unreachable nodes
+		foreach ($aUnreachableObjects as $oObj)
+		{
+			$sNodeId = RelationObjectNode::MakeId($oObj);
+			$oNode = $this->GetNode($sNodeId);
+			if($oNode)
+			{
+				$oNode->SetProperty('is_reached_allowed', false);
+			}
+			else
+			{
+			}
+		}
+		
 		// Determine the reached nodes
 		foreach ($this->aSourceNodes as $oSourceNode)
 		{
@@ -437,4 +452,28 @@ class RelationGraph extends SimpleGraph
 		}
 		return $oRet;
 	}
+	
+	/**
+	 * Get the objects referenced by the graph as a hash array: 'class' => array of objects
+	 * @return Ambigous <multitype:multitype: , unknown>
+	 */
+	public function GetObjectsByClass()
+	{
+		$aResults = array();
+		$oIterator = new RelationTypeIterator($this, 'Node');
+		foreach($oIterator as $oNode)
+		{
+			$oObj = $oNode->GetProperty('object'); // Some nodes (Redundancy Nodes and Group) do not contain an object
+			if ($oObj)
+			{
+				$sObjClass  = get_class($oObj);
+				if (!array_key_exists($sObjClass, $aResults))
+				{
+					$aResults[$sObjClass] = array();
+				}
+				$aResults[$sObjClass][] = $oObj;
+			}
+		}
+		return $aResults;		
+	}	
 }

+ 18 - 33
datamodels/2.x/itop-change-mgmt-itil/datamodel.itop-change-mgmt-itil.xml

@@ -932,47 +932,32 @@
           <static>false</static>
           <access>protected</access>
           <type>Overload-DBObject</type>
-          <code><![CDATA[	protected function OnInsert()
+          <code><![CDATA[
+    protected function OnInsert()
 	{
-		$oToNotify = $this->Get('contacts_list');
-		$oToImpact = $this->Get('functionalcis_list');
-
-		$oImpactedInfras = DBObjectSet::FromLinkSet($this, 'functionalcis_list', 'functionalci_id');
-	
-		$aComputed = $oImpactedInfras->GetRelatedObjects('impacts', 10);
-	
-		if (isset($aComputed['FunctionalCI']) && is_array($aComputed['FunctionalCI']))
-		{
-			foreach($aComputed['FunctionalCI'] as $iKey => $oObject)
-			{
-				$oNewLink = new lnkFunctionalCIToTicket();
-				$oNewLink->Set('functionalci_id', $iKey);
-				$oToImpact->AddObject($oNewLink);
-			}
-		}
-		if (isset($aComputed['Contact']) && is_array($aComputed['Contact']))
-		{
-			foreach($aComputed['Contact'] as $iKey => $oObject)
-			{
-				$oNewLink = new lnkContactToTicket();
-				$oNewLink->Set('contact_id', $iKey);
-				$oNewLink->Set('role', 'contact automatically computed');
-				$oToNotify->AddObject($oNewLink);
-			}
-		}
-
+        parent::OnInsert();
+        $this->UpdateImpactedItems();
 		$this->Set('creation_date', time());
 		$this->Set('last_update', time());
-	}]]></code>
+	}]]>
+          </code>
         </method>
         <method id="OnUpdate">
           <static>false</static>
           <access>protected</access>
           <type>Overload-DBObject</type>
-          <code><![CDATA[	protected function OnUpdate()
-	{
-		$this->Set('last_update', time());
-	}]]></code>
+          <code><![CDATA[
+    protected function OnUpdate()
+  	{
+        parent::OnUpdate();
+        $aChanges = $this->ListChanges();
+        if (array_key_exists('functionalcis_list', $aChanges))
+        {
+            $this->UpdateImpactedItems();
+        }
+  	    $this->Set('last_update', time());
+  	}]]>
+          </code>
         </method>
       </methods>
       <presentation>

+ 12 - 29
datamodels/2.x/itop-change-mgmt/datamodel.itop-change-mgmt.xml

@@ -498,35 +498,11 @@
           <static>false</static>
           <access>protected</access>
           <type>Overload-DBObject</type>
-          <code><![CDATA[	protected function OnInsert()
+          <code><![CDATA[
+    protected function OnInsert()
 	{
-		$oToNotify = $this->Get('contacts_list');
-		$oToImpact = $this->Get('functionalcis_list');
-
-		$oImpactedInfras = DBObjectSet::FromLinkSet($this, 'functionalcis_list', 'functionalci_id');
-	
-		$aComputed = $oImpactedInfras->GetRelatedObjects('impacts', 10);
-	
-		if (isset($aComputed['FunctionalCI']) && is_array($aComputed['FunctionalCI']))
-		{
-			foreach($aComputed['FunctionalCI'] as $iKey => $oObject)
-			{
-				$oNewLink = new lnkFunctionalCIToTicket();
-				$oNewLink->Set('functionalci_id', $iKey);
-				$oToImpact->AddObject($oNewLink);
-			}
-		}
-		if (isset($aComputed['Contact']) && is_array($aComputed['Contact']))
-		{
-			foreach($aComputed['Contact'] as $iKey => $oObject)
-			{
-				$oNewLink = new lnkContactToTicket();
-				$oNewLink->Set('contact_id', $iKey);
-				$oNewLink->Set('role', 'contact automatically computed');
-				$oToNotify->AddObject($oNewLink);
-			}
-		}
-
+        parent::OnInsert();
+        $this->UpdateImpactedItems();
 		$this->Set('creation_date', time());
 		$this->Set('last_update', time());
 	}]]></code>
@@ -535,8 +511,15 @@
           <static>false</static>
           <access>protected</access>
           <type>Overload-DBObject</type>
-          <code><![CDATA[	protected function OnUpdate()
+          <code><![CDATA[
+    protected function OnUpdate()
 	{
+        parent::OnUpdate();
+        $aChanges = $this->ListChanges();
+        if (array_key_exists('functionalcis_list', $aChanges))
+        {
+            $this->UpdateImpactedItems();
+        }
 		$this->Set('last_update', time());
 	}]]></code>
         </method>

+ 16 - 49
datamodels/2.x/itop-request-mgmt-itil/datamodel.itop-request-mgmt-itil.xml

@@ -1361,61 +1361,21 @@
           <static>false</static>
           <access>public</access>
           <type>LifecycleAction</type>
-          <code><![CDATA[	public function ComputeImpactedItems()
+          <code><![CDATA[
+    public function ComputeImpactedItems()
 	{
-		$oToNotify = $this->Get('contacts_list');
-		$oToImpact = $this->Get('functionalcis_list');
-
-		$oImpactedInfras = DBObjectSet::FromLinkSet($this, 'functionalcis_list', 'functionalci_id');
-
-        $oGraph = $oImpactedInfras->GetRelatedObjectsDown('impacts',10, true /* bEnableRedundancy */);
-        $oIterator = new RelationTypeIterator($oGraph, 'Node');
-        foreach($oIterator as $oNode)
-        {
-            if($oNode instanceof RelationObjectNode)
-            {
-                if ($oNode->GetProperty('is_reached') && (!$oNode->GetProperty('source')))
-                {
-                    $oObj = $oNode->GetProperty('object');
-                    $sRootClass = MetaModel::GetRootClass(get_class($oObj));
-                    if (!array_key_exists($sRootClass, $aComputed))
-                    {
-                        $aComputed[$sRootClass] = array();
-                    }
-                    $aComputed[$sRootClass][$oObj->GetKey()] = $oObj;
-                }
-            }
-        }
-
-		if (isset($aComputed['FunctionalCI']) && is_array($aComputed['FunctionalCI']))
-		{
-			foreach($aComputed['FunctionalCI'] as $iKey => $oObject)
-			{
-				$oNewLink = new lnkFunctionalCIToTicket();
-				$oNewLink->Set('functionalci_id', $iKey);
-				$oNewLink->Set('impact', 'potentially impacted (automatically computed)');
-				$oToImpact->AddObject($oNewLink);
-			}
-		}
-		if (isset($aComputed['Contact']) && is_array($aComputed['Contact']))
-		{
-			foreach($aComputed['Contact'] as $iKey => $oObject)
-			{
-				$oNewLink = new lnkContactToTicket();
-				$oNewLink->Set('contact_id', $iKey);
-				$oNewLink->Set('role', 'contact automatically computed');
-				$oToNotify->AddObject($oNewLink);
-			}
-		}
-		parent::OnInsert();
+        // This method is kept for backward compatibility
+        // in case a delta redefines it, but you may call
+        // UpdateImpactedItems directly
+		$this->UpdateImpactedItems();
 	}]]></code>
         </method>
         <method id="OnInsert">
           <static>false</static>
           <access>protected</access>
           <type>Overload-DBObject</type>
-          <code><![CDATA[	protected function OnInsert()
-
+          <code><![CDATA[
+    protected function OnInsert()
 	{
 		$this->ComputeImpactedItems();
 		$this->Set('last_update', time());
@@ -1426,8 +1386,15 @@
           <static>false</static>
           <access>protected</access>
           <type>Overload-DBObject</type>
-          <code><![CDATA[	protected function OnUpdate()
+          <code><![CDATA[
+    protected function OnUpdate()
 	{
+        parent::OnUpdate();
+        $aChanges = $this->ListChanges();
+        if (array_key_exists('functionalcis_list', $aChanges))
+        {
+            $this->UpdateImpactedItems();
+        }
 		$this->Set('last_update', time());
 		$this->UpdateChildRequestLog();
 	}]]></code>

+ 17 - 49
datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml

@@ -1363,62 +1363,23 @@
           <static>false</static>
           <access>public</access>
           <type>LifecycleAction</type>
-          <code><![CDATA[	public function ComputeImpactedItems()
+          <code><![CDATA[
+    public function ComputeImpactedItems()
 	{
-		$oToNotify = $this->Get('contacts_list');
-		$oToImpact = $this->Get('functionalcis_list');
-
-		$oImpactedInfras = DBObjectSet::FromLinkSet($this, 'functionalcis_list', 'functionalci_id');
-
-        $oGraph = $oImpactedInfras->GetRelatedObjectsDown('impacts',10, true /* bEnableRedundancy */);
-        $oIterator = new RelationTypeIterator($oGraph, 'Node');
-        foreach($oIterator as $oNode)
-        {
-            if($oNode instanceof RelationObjectNode)
-            {
-                if ($oNode->GetProperty('is_reached') && (!$oNode->GetProperty('source')))
-                {
-                    $oObj = $oNode->GetProperty('object');
-                    $sRootClass = MetaModel::GetRootClass(get_class($oObj));
-                    if (!array_key_exists($sRootClass, $aComputed))
-                    {
-                        $aComputed[$sRootClass] = array();
-                    }
-                    $aComputed[$sRootClass][$oObj->GetKey()] = $oObj;
-                }
-            }
-        }
-        
-		if (isset($aComputed['FunctionalCI']) && is_array($aComputed['FunctionalCI']))
-		{
-			foreach($aComputed['FunctionalCI'] as $iKey => $oObject)
-			{
-				$oNewLink = new lnkFunctionalCIToTicket();
-				$oNewLink->Set('functionalci_id', $iKey);
-				$oNewLink->Set('impact', 'potentially impacted (automatically computed)');
-				$oToImpact->AddObject($oNewLink);
-			}
-		}
-		if (isset($aComputed['Contact']) && is_array($aComputed['Contact']))
-		{
-			foreach($aComputed['Contact'] as $iKey => $oObject)
-			{
-				$oNewLink = new lnkContactToTicket();
-				$oNewLink->Set('contact_id', $iKey);
-				$oNewLink->Set('role', 'contact automatically computed');
-				$oToNotify->AddObject($oNewLink);
-			}
-		}
-		parent::OnInsert();
+        // This method is kept for backward compatibility
+        // in case a delta redefines it, but you may call
+        // UpdateImpactedItems directly
+		$this->UpdateImpactedItems();
 	}]]></code>
         </method>
         <method id="OnInsert">
           <static>false</static>
           <access>protected</access>
           <type>Overload-DBObject</type>
-          <code><![CDATA[	protected function OnInsert()
-
+          <code><![CDATA[
+    protected function OnInsert()
 	{
+        parent::OnInsert();
 		$this->ComputeImpactedItems();
 		$this->Set('last_update', time());
 		$this->Set('start_date', time());
@@ -1428,8 +1389,15 @@
           <static>false</static>
           <access>protected</access>
           <type>Overload-DBObject</type>
-          <code><![CDATA[	protected function OnUpdate()
+          <code><![CDATA[
+   	protected function OnUpdate()
 	{
+        parent::OnUpdate();
+        $aChanges = $this->ListChanges();
+        if (array_key_exists('functionalcis_list', $aChanges))
+        {
+            $this->UpdateImpactedItems();
+        }
 		$this->Set('last_update', time());
 		$this->UpdateChildRequestLog();
 	}]]></code>

+ 29 - 6
datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml

@@ -17,6 +17,7 @@
   <classes>
     <class id="Ticket" _delta="define">
       <parent>cmdbAbstractObject</parent>
+       <php_parent><name>_Ticket</name></php_parent>
       <properties>
         <comment><![CDATA[/**
  * Persistent classes for a CMDB
@@ -366,6 +367,17 @@
           <default_value/>
           <is_null_allowed>true</is_null_allowed>
         </field>
+        <field id="role_code" xsi:type="AttributeEnum">
+          <values>
+            <value id="manual">manual</value>
+            <value id="computed">computed</value>
+            <value id="do_not_notify">do_not_notify</value>
+          </values>
+          <sql>impact_code</sql>
+          <default_value>manual</default_value>
+          <is_null_allowed>false</is_null_allowed>
+          <display_style>list</display_style>
+        </field>
       </fields>
       <methods/>
       <presentation>
@@ -377,7 +389,7 @@
             <item id="contact_id">
               <rank>20</rank>
             </item>
-            <item id="role">
+            <item id="role_code">
               <rank>30</rank>
             </item>
           </items>
@@ -390,7 +402,7 @@
             <item id="contact_id">
               <rank>20</rank>
             </item>
-            <item id="role">
+            <item id="role_code">
               <rank>30</rank>
             </item>
           </items>
@@ -403,7 +415,7 @@
             <item id="contact_id">
               <rank>20</rank>
             </item>
-            <item id="role">
+            <item id="role_code">
               <rank>30</rank>
             </item>
           </items>
@@ -465,6 +477,17 @@
           <default_value/>
           <is_null_allowed>true</is_null_allowed>
         </field>
+        <field id="impact_code" xsi:type="AttributeEnum">
+          <values>
+            <value id="manual">manual</value>
+            <value id="computed">computed</value>
+            <value id="not_impacted">not_impacted</value>
+          </values>
+          <sql>impact_code</sql>
+          <default_value>manual</default_value>
+          <is_null_allowed>false</is_null_allowed>
+          <display_style>list</display_style>
+        </field>
       </fields>
       <methods/>
       <presentation>
@@ -476,7 +499,7 @@
             <item id="functionalci_id">
               <rank>20</rank>
             </item>
-            <item id="impact">
+            <item id="impact_code">
               <rank>30</rank>
             </item>
           </items>
@@ -489,7 +512,7 @@
             <item id="functionalci_id">
               <rank>20</rank>
             </item>
-            <item id="impact">
+            <item id="impact_code">
               <rank>30</rank>
             </item>
           </items>
@@ -502,7 +525,7 @@
             <item id="functionalci_id">
               <rank>20</rank>
             </item>
-            <item id="impact">
+            <item id="impact_code">
               <rank>30</rank>
             </item>
           </items>

+ 11 - 2
datamodels/2.x/itop-tickets/de.dict.itop-tickets.php

@@ -58,22 +58,31 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
 	'Class:Ticket/Attribute:workorders_list+' => '',
 	'Class:Ticket/Attribute:finalclass' => 'Typ',
 	'Class:Ticket/Attribute:finalclass+' => '',
+	'Ticket:ImpactAnalysis' => 'Impact Analysis~~',
 	'Class:lnkContactToTicket' => 'Verknüpfung Kontakt/Ticket',
 	'Class:lnkContactToTicket+' => '',
 	'Class:lnkContactToTicket/Attribute:ticket_id' => 'Ticket',
 	'Class:lnkContactToTicket/Attribute:ticket_id+' => '',
 	'Class:lnkContactToTicket/Attribute:contact_id' => 'Kontakt',
 	'Class:lnkContactToTicket/Attribute:contact_id+' => '',
-	'Class:lnkContactToTicket/Attribute:role' => 'Rolle',
+	'Class:lnkContactToTicket/Attribute:role' => 'Rolle (Text)',
 	'Class:lnkContactToTicket/Attribute:role+' => '',
+	'Class:lnkContactToTicket/Attribute:role_code' => 'Rolle',
+	'Class:lnkContactToTicket/Attribute:role_code/Value:manual' => 'Added manually~~',
+	'Class:lnkContactToTicket/Attribute:role_code/Value:computed' => 'Computed~~',
+	'Class:lnkContactToTicket/Attribute:role_code/Value:do_not_notify' => 'Do not notify~~',
 	'Class:lnkFunctionalCIToTicket' => 'Verknüpfung FunctionalCI/Ticket',
 	'Class:lnkFunctionalCIToTicket+' => '',
 	'Class:lnkFunctionalCIToTicket/Attribute:ticket_id' => 'Ticket',
 	'Class:lnkFunctionalCIToTicket/Attribute:ticket_id+' => '',
 	'Class:lnkFunctionalCIToTicket/Attribute:functionalci_id' => 'CI',
 	'Class:lnkFunctionalCIToTicket/Attribute:functionalci_id+' => '',
-	'Class:lnkFunctionalCIToTicket/Attribute:impact' => 'Auswirkung',
+	'Class:lnkFunctionalCIToTicket/Attribute:impact' => 'Auswirkung (Text)',
 	'Class:lnkFunctionalCIToTicket/Attribute:impact+' => '',
+	'Class:lnkFunctionalCIToTicket/Attribute:impact_code' => 'Auswirkung',
+	'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:manual' => 'Added manually~~',
+	'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:computed' => 'Computed~~',
+	'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:not_impacted' => 'Not impacted~~',
 	'Class:WorkOrder' => 'Arbeitsauftrag',
 	'Class:WorkOrder+' => '',
 	'Class:WorkOrder/Attribute:name' => 'Name',

+ 11 - 2
datamodels/2.x/itop-tickets/en.dict.itop-tickets.php

@@ -82,6 +82,7 @@ Dict::Add('EN US', 'English', 'English', array(
 	'Class:Ticket/Attribute:workorders_list+' => 'All the work orders for this ticket',
 	'Class:Ticket/Attribute:finalclass' => 'Type',
 	'Class:Ticket/Attribute:finalclass+' => '',
+	'Ticket:ImpactAnalysis' => 'Impact Analysis',
 ));
 
 
@@ -100,8 +101,12 @@ Dict::Add('EN US', 'English', 'English', array(
 	'Class:lnkContactToTicket/Attribute:contact_id+' => '',
 	'Class:lnkContactToTicket/Attribute:contact_email' => 'Contact Email',
 	'Class:lnkContactToTicket/Attribute:contact_email+' => '',
-	'Class:lnkContactToTicket/Attribute:role' => 'Role',
+	'Class:lnkContactToTicket/Attribute:role' => 'Role (text)',
 	'Class:lnkContactToTicket/Attribute:role+' => '',
+	'Class:lnkContactToTicket/Attribute:role_code' => 'Role',
+	'Class:lnkContactToTicket/Attribute:role_code/Value:manual' => 'Added manually',
+	'Class:lnkContactToTicket/Attribute:role_code/Value:computed' => 'Computed',
+	'Class:lnkContactToTicket/Attribute:role_code/Value:do_not_notify' => 'Do not notify',
 ));
 
 //
@@ -119,8 +124,12 @@ Dict::Add('EN US', 'English', 'English', array(
 	'Class:lnkFunctionalCIToTicket/Attribute:functionalci_id+' => '',
 	'Class:lnkFunctionalCIToTicket/Attribute:functionalci_name' => 'CI Name',
 	'Class:lnkFunctionalCIToTicket/Attribute:functionalci_name+' => '',
-	'Class:lnkFunctionalCIToTicket/Attribute:impact' => 'Impact',
+	'Class:lnkFunctionalCIToTicket/Attribute:impact' => 'Impact (text)',
 	'Class:lnkFunctionalCIToTicket/Attribute:impact+' => '',
+	'Class:lnkFunctionalCIToTicket/Attribute:impact_code' => 'Impact',
+	'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:manual' => 'Added manually',
+	'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:computed' => 'Computed',
+	'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:not_impacted' => 'Not impacted',
 ));
 
 

+ 11 - 2
datamodels/2.x/itop-tickets/fr.dict.itop-tickets.php

@@ -69,6 +69,7 @@ Dict::Add('FR FR', 'French', 'Français', array(
 	'Class:Ticket/Attribute:workorders_list+' => '',
 	'Class:Ticket/Attribute:finalclass' => 'Type',
 	'Class:Ticket/Attribute:finalclass+' => '',
+	'Ticket:ImpactAnalysis' => 'Analyse d\'Impact',
 ));
 
 
@@ -87,8 +88,12 @@ Dict::Add('FR FR', 'French', 'Français', array(
 	'Class:lnkContactToTicket/Attribute:contact_id+' => '',
 	'Class:lnkContactToTicket/Attribute:contact_email' => 'Email Contact',
 	'Class:lnkContactToTicket/Attribute:contact_email+' => '',
-	'Class:lnkContactToTicket/Attribute:role' => 'Rôle',
+	'Class:lnkContactToTicket/Attribute:role' => 'Rôle (texte)',
 	'Class:lnkContactToTicket/Attribute:role+' => '',
+	'Class:lnkContactToTicket/Attribute:role_code' => 'Rôle',
+	'Class:lnkContactToTicket/Attribute:role_code/Value:manual' => 'Ajouté manuellement',
+	'Class:lnkContactToTicket/Attribute:role_code/Value:computed' => 'Calculé',
+	'Class:lnkContactToTicket/Attribute:role_code/Value:do_not_notify' => 'Ne pas notifier',
 ));
 
 //
@@ -106,8 +111,12 @@ Dict::Add('FR FR', 'French', 'Français', array(
 	'Class:lnkFunctionalCIToTicket/Attribute:functionalci_id+' => '',
 	'Class:lnkFunctionalCIToTicket/Attribute:functionalci_name' => 'Nom CI',
 	'Class:lnkFunctionalCIToTicket/Attribute:functionalci_name+' => '',
-	'Class:lnkFunctionalCIToTicket/Attribute:impact' => 'Impact',
+	'Class:lnkFunctionalCIToTicket/Attribute:impact' => 'Impact (texte)',
 	'Class:lnkFunctionalCIToTicket/Attribute:impact+' => '',
+	'Class:lnkFunctionalCIToTicket/Attribute:impact_code' => 'Impact',
+	'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:manual' => 'Ajouté manuellement',
+	'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:computed' => 'Calculé',
+	'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:not_impacted' => 'Non impacté',
 ));
 
 

+ 120 - 0
datamodels/2.x/itop-tickets/main.itop-tickets.php

@@ -127,4 +127,124 @@ class ResponseTicketTTR extends ResponseTicketSLT implements iMetricComputer
 	}
 }
 
+
+class _Ticket extends cmdbAbstractObject
+{
+
+	public function UpdateImpactedItems()
+	{
+		$oContactsSet = $this->Get('contacts_list');
+		$oCIsSet = $this->Get('functionalcis_list');
+		
+		$aCIsToImpactCode = array();
+		$aSources = array();
+		$aExcluded = array();
+		
+		$oCIsSet->Rewind();
+		while ($oLink = $oCIsSet->Fetch())
+		{
+			$iKey = $oLink->Get('functionalci_id');
+			$aCIsToImpactCode[$iKey] = $oLink->Get('impact_code');
+			if ($oLink->Get('impact_code') == 'manual')
+			{
+				$oObj = MetaModel::GetObject('FunctionalCI', $iKey);
+				$aSources[$iKey] = $oObj;
+			}
+			else if ($oLink->Get('impact_code') == 'not_impacted')
+			{
+				$oObj = MetaModel::GetObject('FunctionalCI', $iKey);
+				$aExcluded[$iKey] = $oObj;
+			}
+		}
+		
+		$aContactsToRoleCode = array();
+		$oContactsSet->Rewind();
+		while ($oLink = $oContactsSet->Fetch())
+		{
+			$iKey = $oLink->Get('contact_id');
+			$aContactsToRoleCode[$iKey] = $oLink->Get('role_code');
+			if ($oLink->Get('role_code') == 'do_not_notify')
+			{
+				$oObj = MetaModel::GetObject('Contact', $iKey);
+				$aExcluded[$iKey] = $oObj;
+			}
+		}
+		
+		$oNewCIsSet = DBObjectSet::FromScratch('lnkFunctionalCIToTicket');
+		foreach($aCIsToImpactCode as $iKey => $sImpactCode)
+		{
+			if ($sImpactCode != 'computed')
+			{
+				$oNewLink = new lnkFunctionalCIToTicket();
+				$oNewLink->Set('functionalci_id', $iKey);
+				$oNewLink->Set('impact_code', $sImpactCode);
+				$oNewCIsSet->AddObject($oNewLink);				
+			}
+		}
+		
+		$oNewContactsSet = DBObjectSet::FromScratch('lnkContactToTicket');
+		foreach($aContactsToRoleCode as $iKey => $sImpactCode)
+		{
+			if ($sImpactCode != 'computed')
+			{
+				$oNewLink = new lnkContactToTicket();
+				$oNewLink->Set('contact_id', $iKey);
+				$oNewLink->Set('role_code', $sImpactCode);
+				$oNewContactsSet->AddObject($oNewLink);
+			}
+		}
+		
+		$oContactsSet = DBObjectSet::FromScratch('lnkContactToTicket');
+		$oGraph = MetaModel::GetRelatedObjectsDown('impacts', $aSources, 10, true /* bEnableRedundancy */, $aExcluded);
+		$oIterator = new RelationTypeIterator($oGraph, 'Node');
+		foreach ($oIterator as $oNode)
+		{
+			if ( ($oNode instanceof RelationObjectNode) && ($oNode->GetProperty('is_reached')) && (!$oNode->GetProperty('source')))
+			{
+				$oObj = $oNode->GetProperty('object');
+				$iKey = $oObj->GetKey();
+				$sRootClass = MetaModel::GetRootClass(get_class($oObj));
+				switch ($sRootClass)
+				{
+					case 'FunctionalCI':
+					// Only link FunctionalCIs which are not already linked to the ticket
+					if (!array_key_exists($iKey, $aCIsToImpactCode) || ($aCIsToImpactCode[$iKey] != 'not_impacted'))
+					{
+						$oNewLink = new lnkFunctionalCIToTicket();
+						$oNewLink->Set('functionalci_id', $iKey);
+						$oNewLink->Set('impact_code', 'computed');
+						$oNewCIsSet->AddObject($oNewLink);
+					}
+					break;
+					
+					case 'Contact':
+					// Only link Contacts which are not already linked to the ticket
+					if (!array_key_exists($iKey, $aContactsToRoleCode) || ($aCIsToImpactCode[$iKey] != 'do_not_notify'))
+					{
+						$oNewLink = new lnkContactToTicket();
+						$oNewLink->Set('contact_id', $iKey);
+						$oNewLink->Set('role_code', 'computed');
+						$oNewContactsSet->AddObject($oNewLink);
+					}
+					break;
+				}
+			}
+		}
+		$this->Set('functionalcis_list', $oNewCIsSet);
+		$this->Set('contacts_list', $oNewContactsSet);
+	}
+	
+	public function DisplayBareRelations(WebPage $oPage, $bEditMode = false)
+	{
+		parent::DisplayBareRelations($oPage, $bEditMode);
+		if (!$bEditMode)
+		{
+			$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/fraphael.js');
+			$oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/jquery.contextMenu.css');
+			$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.contextMenu.js');
+			$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/simple_graph.js');
+			$oPage->AddAjaxTab(Dict::S('Ticket:ImpactAnalysis'), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=ticket_impact&class='.get_class($this).'&id='.$this->GetKey(), true);
+		}
+	}
+}
 ?>

+ 2 - 2
datamodels/2.x/itop-tickets/module.itop-tickets.php

@@ -3,7 +3,7 @@
 
 SetupWebPage::AddModule(
 	__FILE__,
-	'itop-tickets/2.1.0',
+	'itop-tickets/2.2.0',
 	array(
 		// Identification
 		//
@@ -22,8 +22,8 @@ SetupWebPage::AddModule(
 		// Components
 		//
 		'datamodel' => array(
-			'model.itop-tickets.php',
 			'main.itop-tickets.php',
+			'model.itop-tickets.php',
 		),
 		'data.struct' => array(
 	//		'data.struct.ta-actions.xml',

+ 1 - 0
dictionaries/de.dictionary.itop.ui.php

@@ -777,6 +777,7 @@ Wenn Aktionen mit Trigger verknüpft sind, bekommt jede Aktion eine Auftragsnumm
 	'UI:RelationGroups' => 'Gruppen',
 	'UI:RelationGroupNumber_N' => 'Gruppe #%1$d~~',
 	'UI:Relation:ExportAsPDF' => 'Export as PDF...~~',
+	'UI:RelationOption:GroupingThreshold' => 'Grouping threshold~~',
 	'UI:Relation:ExportAsDocument' => 'Export as Document...~~',
 	'UI:Relation:DrillDown' => 'Details...~~',
 	'UI:Relation:PDFExportOptions' => 'PDF Export Options~~',

+ 1 - 0
dictionaries/dictionary.itop.ui.php

@@ -970,6 +970,7 @@ When associated with a trigger, each action is given an "order" number, specifyi
 	'UI:ElementsDisplayed' => 'Filtering',
 	'UI:RelationGroupNumber_N' => 'Group #%1$d',
 	'UI:Relation:ExportAsPDF' => 'Export as PDF...',
+	'UI:RelationOption:GroupingThreshold' => 'Grouping threshold',
 	'UI:Relation:ExportAsDocument' => 'Export as Document...',
 	'UI:Relation:DrillDown' => 'Details...',
 	'UI:Relation:PDFExportOptions' => 'PDF Export Options',

+ 4 - 3
dictionaries/fr.dictionary.itop.ui.php

@@ -812,10 +812,11 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
 	'UI:RelationGroups' => 'Groupes',
 	'UI:ElementsDisplayed' => 'Filtrage',
 	'UI:RelationGroupNumber_N' => 'Groupe n°%1$d',
-	'UI:Relation:ExportAsPDF' => 'Exportation en PDF...',
-	'UI:Relation:ExportAsDocument' => 'Exportation comme Document...',
+	'UI:Relation:ExportAsPDF' => 'Exporter en PDF...',
+	'UI:RelationOption:GroupingThreshold' => 'Seuil de groupage',
+	'UI:Relation:ExportAsDocument' => 'Exporter comme Document...',
 	'UI:Relation:DrillDown' => 'Détails...',
-	'UI:Relation:PDFExportOptions' => 'Options de l\'Exportation PDF',
+	'UI:Relation:PDFExportOptions' => 'Options de l\'export en PDF',
 	'UI:Relation:Key' => 'Légende',
 	'UI:Relation:Comments' => 'Commentaires',
 	'UI:RelationOption:Title' => 'Titre',

+ 56 - 16
js/simple_graph.js

@@ -16,13 +16,24 @@ $(function()
 			align: 'center',
 			'vertical-align': 'middle',
 			source_url: null,
+			sources: {},
+			excluded: {},
 			export_as_pdf: null,
 			page_format: { label: 'Page Format:', values: { A3: 'A3', A4: 'A4', Letter: 'Letter' }, 'default': 'A4'},
 			page_orientation: { label: 'Page Orientation:', values: { P: 'Portait', L: 'Landscape' }, 'default': 'L' },
-			labels: { export_pdf_title: 'PDF Export Options', cancel: 'Cancel', 'export': 'Export', title: 'Document Title', include_list: 'Include the list of objects', comments: 'Comments' },
+			labels: {
+				export_pdf_title: 'PDF Export Options',
+				cancel: 'Cancel', 'export': 'Export',
+				title: 'Document Title',
+				include_list: 'Include the list of objects',
+				comments: 'Comments',
+				grouping_threshold: 'Grouping Threshold',
+				refresh: 'Refresh'
+			},
 			export_as_document: null,
 			drill_down: null,
-			excluded: []
+			grouping_threshold: 10,
+			excluded_classes: []
 		},
 	
 		// the constructor
@@ -352,7 +363,10 @@ $(function()
 		_create_toolkit_menu: function()
 		{
 			var sPopupMenuId = 'tk_graph'+this.element.attr('id');
-			var sHtml = '<div class="itop_popup toolkit_menu graph" style="font-size: 12px;" id="'+sPopupMenuId+'"><ul><li><img src="../images/toolkit_menu.png"><ul>';
+			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 += '<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)
 			{
 				sHtml += '<li><a href="#" id="'+sPopupMenuId+'_pdf">'+this.options.export_as_pdf.label+'</a></li>';			
@@ -363,16 +377,17 @@ $(function()
 			}
 			//sHtml += '<li><a href="#" id="'+sPopupMenuId+'_reload">Refresh</a></li>';
 			sHtml += '</ul></li></ul></div>';
+			sHtml += '</div>';
 			
 			this.element.before(sHtml);
-			$('#'+sPopupMenuId).popupmenu();
+			$('#'+sPopupMenuId+'>ul').popupmenu();
 			
 			
 			var me = this;
 			$('#'+sPopupMenuId+'_pdf').click(function() { me.export_as_pdf(); });
 			$('#'+sPopupMenuId+'_document').click(function() { me.export_as_document(); });
-			$('#'+sPopupMenuId+'_reload').click(function() { me.reload(); });
-			
+			$('#'+sId+'_grouping_threshold').spinner({ min: 2});
+			$('#'+sId+'_refresh_btn').button().click(function() { me.reload(); });
 		},
 		_build_context_menus: function()
 		{
@@ -400,13 +415,16 @@ $(function()
 		        	{
 		        		case 'group':
 		        		var sGroupIndex = oNode.group_index;
-		        		oResult = {
-	        				callback: function(key, options) {
-	        					var me = $('.itop-simple-graph').data('itopSimple_graph'); // need a live value
-	        					me.show_group('relation_group_'+sGroupIndex);
-	        				},
-	        				items: { 'show': {name: me.options.drill_down.label } }
-	        			};
+		        		if( $('#relation_group_'+sGroupIndex).length > 0)
+		        		{
+			        		oResult = {
+			        				callback: function(key, options) {
+			        					var me = $('.itop-simple-graph').data('itopSimple_graph'); // need a live value
+			        					me.show_group('relation_group_'+sGroupIndex);
+			        				},
+			        				items: { 'show': {name: me.options.drill_down.label } }
+			        			};		        			
+		        		}
 						break;
 						
 		        		case 'icon':
@@ -438,10 +456,25 @@ $(function()
 				oPositions[this.aNodes[k].id] = {x: this.aNodes[k].x, y: this.aNodes[k].y };
 			}
 			var sHtmlForm = '<div id="PDFExportDlg'+this.element.attr('id')+'"><form id="graph_'+this.element.attr('id')+'_export_as_pdf" target="_blank" action="'+this.options.export_as_pdf.url+'" method="post">';
+			sHtmlForm += '<input type="hidden" name="g" value="'+this.options.grouping_threshold+'">';
 			sHtmlForm += '<input type="hidden" name="positions" value="">';
-			for(k in this.options.excluded)
+			for(k in this.options.excluded_classes)
+			{
+				sHtmlForm += '<input type="hidden" name="excluded_classes[]" value="'+this.options.excluded_classes[k]+'">';				
+			}
+			for(var k1 in this.options.sources)
 			{
-				sHtmlForm += '<input type="hidden" name="excluded[]" value="'+this.options.excluded[k]+'">';				
+				for(var k2 in this.options.sources[k1])
+				{
+					sHtmlForm += '<input type="hidden" name="sources['+k1+'][]" value="'+this.options.sources[k1][k2]+'">';									
+				}
+			}
+			for(var k1 in this.options.excluded)
+			{
+				for(var k2 in this.options.excluded[k1])
+				{
+					sHtmlForm += '<input type="hidden" name="excluded['+k1+'][]" value="'+this.options.excluded[k1][k2]+'">';									
+				}
 			}
 			sHtmlForm += '<table>';
 			sHtmlForm += '<tr><td>'+this.options.page_format.label+'</td><td><select name="p">';
@@ -507,9 +540,16 @@ $(function()
 		{
 			this.options.load_from_url = sUrl;
 			var me = this;
+			var sId = this.element.attr('id');
+			this.options.grouping_threshold = $('#'+sId+'_grouping_threshold').val();
+			if (this.options.grouping_threshold < 2)
+			{
+				this.options.grouping_threshold = 2;
+				$('#'+sId+'_grouping_threshold').val(this.options.grouping_threshold);
+			}
 			this.element.closest('.ui-tabs').tabs({ heightStyle: "fill" });
 			this.oPaper.rect(0, 0, this.element.width(), this.element.height()).attr({fill: '#000', opacity: 0.4, 'stroke-width': 0});
-			$.post(sUrl, {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 }, function(data) {
 				me.load(data);
 			}, 'json');
 		},

+ 10 - 175
pages/UI.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -262,163 +262,11 @@ function DisplayNavigatorGroupTab($oP, $aGroups, $sRelation, $oObj)
 	}
 }
 
-function DisplayNavigatorGraphicsTab($oP, $aResults, $oDisplayGraph, $sClass, $id, $sRelation, $oAppContext, $bDirectionDown)
-{
-	$oP->SetCurrentTab(Dict::S('UI:RelationshipGraph'));
-
-	$oP->add("<div id=\"ds_flash\" class=\"SearchDrawer\" style=\"display:none;\">\n");
-	$oP->add_ready_script(
-<<<EOF
-	$( "#tabbedContent_0" ).tabs({ heightStyle: "fill" });
-	$("#dh_flash").click( function() {
-		$("#ds_flash").slideToggle('normal', function() { $("#ds_flash").parent().resize(); } );
-		$("#dh_flash").toggleClass('open');
-	});
-EOF
-	);
-	$aSortedElements = array();
-	foreach($aResults as $sClassIdx => $aObjects)
-	{
-		foreach($aObjects as $oCurrObj)
-		{
-			$sSubClass = get_class($oCurrObj);
-			$aSortedElements[$sSubClass] = MetaModel::GetName($sSubClass);
-		}
-	}
-		
-	asort($aSortedElements);
-	$idx = 0;
-	foreach($aSortedElements as $sSubClass => $sClassName)
-	{
-		$oP->add("<span style=\"padding-right:2em; white-space:nowrap;\"><input type=\"checkbox\" id=\"exclude_$idx\" name=\"excluded[]\" value=\"$sSubClass\" checked onChange=\"$('#ReloadMovieBtn').button('enable')\"><label for=\"exclude_$idx\">&nbsp;".MetaModel::GetClassIcon($sSubClass)."&nbsp;$sClassName</label></span> ");
-		$idx++;	
-	}
-	$oP->add("<p style=\"text-align:right\"><button type=\"button\" id=\"ReloadMovieBtn\" onClick=\"DoReload()\">".Dict::S('UI:Button:Refresh')."</button></p>");
-	$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");
-	
-	$sDirection = utils::ReadParam('d', 'horizontal');
-	$iGroupingThreshold = utils::ReadParam('g', 5);
-		
-	$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/fraphael.js');
-	$oP->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/jquery.contextMenu.css');
-	$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.contextMenu.js');
-	$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/simple_graph.js');
-	try
-	{
-		$oDisplayGraph->InitFromGraphviz();
-		$sExportAsPdfURL = '';
-		if (extension_loaded('gd'))
-		{
-			$sExportAsPdfURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_pdf&relation='.$sRelation.'&direction='.($bDirectionDown ? 'down' : 'up').'&class='.$sClass.'&id='.$id.'&g='.$iGroupingThreshold;
-		}
-		$oAppcontext = new ApplicationContext();
-		$sContext = $oAppContext->GetForLink();
-		$sDrillDownURL = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=details&class=%1$s&id=%2$s&'.$sContext;
-		$sExportAsDocumentURL = '';
-		$sLoadFromURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_json&relation='.$sRelation.'&direction='.($bDirectionDown ? 'down' : 'up').'&class='.$sClass.'&id='.$id.'&g='.$iGroupingThreshold;
-		
-		$sId = 'graph';
-		$oP->add('<div id="'.$sId.'" class="simple-graph"></div>');
-		$aParams = array(
-			'source_url' => $sLoadFromURL,
-			'export_as_pdf' => array('url' => $sExportAsPdfURL, 'label' => Dict::S('UI:Relation:ExportAsPDF')),
-			//'export_as_document' => array('url' => $sExportAsDocumentURL, 'label' => Dict::S('UI:Relation:ExportAsDocument')),
-			'drill_down' => array('url' => $sDrillDownURL, 'label' => Dict::S('UI:Relation:DrillDown')),
-			'labels' => array(
-				'export_pdf_title' => Dict::S('UI:Relation:PDFExportOptions'),
-				'export' => Dict::S('UI:Button:Export'),
-				'cancel' => Dict::S('UI:Button:Cancel'),
-				'title' => Dict::S('UI:RelationOption:Title'),
-				'include_list' => Dict::S('UI:RelationOption:IncludeList'),
-				'comments' => Dict::S('UI:RelationOption:Comments'),
-			),
-			'page_format' => array(
-				'label' => Dict::S('UI:Relation:PDFExportPageFormat'),
-				'values' => array(
-					'A3' => Dict::S('UI:PageFormat_A3'),
-					'A4' => Dict::S('UI:PageFormat_A4'),
-					'Letter' => Dict::S('UI:PageFormat_Letter'),
-				),
-			),
-			'page_orientation' => array(
-				'label' => Dict::S('UI:Relation:PDFExportPageOrientation'),
-				'values' => array(
-					'P' => Dict::S('UI:PageOrientation_Portrait'),
-					'L' => Dict::S('UI:PageOrientation_Landscape'),
-				),
-			),
-		);
-		$oP->add_ready_script("$('#$sId').simple_graph(".json_encode($aParams).");");
-	}
-	catch(Exception $e)
-	{
-		$oP->add('<div>'.$e->getMessage().'</div>');
-	}
-	$oP->add_script(
-<<<EOF
-			
-	function DoReload()
-	{
-		$('#ReloadMovieBtn').button('disable');
-		try
-		{
-			var aExcluded = [];
-			$('input[name^=excluded]').each( function() {
-				if (!$(this).prop('checked'))
-				{
-					aExcluded.push($(this).val());
-				}
-			} );
-			$('#graph').simple_graph('option', {excluded: aExcluded});
-			$('#graph').simple_graph('reload');
-		}
-		catch(err)
-		{
-			alert(err);
-		}
-	}
-EOF
-);
-	$oP->add_ready_script(
-<<<EOF
-	var ajax_request = null;
-
-	$('#ReloadMovieBtn').button().button('disable');
-	
-	function UpdateImpactedObjects(sClass, iId, sRelation)
-	{
-		var class_name = sClass; //$('select[name=class_name]').val();
-		if (class_name != '')
-		{
-			$('#impacted_objects').block();
-	
-			// Make sure that we cancel any pending request before issuing another
-			// since responses may arrive in arbitrary order
-			if (ajax_request != null)
-			{
-				ajax_request.abort();
-				ajax_request = null;
-			}
-	
-			ajax_request = $.get(GetAbsoluteUrlAppRoot()+'pages/xml.navigator.php', { 'class': sClass, id: iId, relation: sRelation, format: 'html' },
-					function(data)
-					{
-						alert('Not yet implemented');
-						$('#impacted_objects').unblock();
-					}
-			);
-		}
-	}
-EOF
-	);
-}
 /***********************************************************************************
  * 
- * Main user interface page, starts here
+ * Main user interface page starts here
  *
- * ***********************************************************************************/
+ ***********************************************************************************/
 try
 {
 	require_once('../approot.inc.php');
@@ -1608,26 +1456,11 @@ EOF
 		}
 		
 
-		$aResults = array();
-		$aGroups = array();
-		$iGroupIdx = 0;
-		$oIterator = new RelationTypeIterator($oRelGraph, 'Node');
-		foreach($oIterator as $oNode)
-		{
-			$oObj = $oNode->GetProperty('object'); // Some nodes (Redundancy Nodes and Group) do not contain an object
-			if ($oObj)
-			{
-				$sObjClass  = get_class($oObj);
-				if (!array_key_exists($sObjClass, $aResults))
-				{
-					$aResults[$sObjClass] = array();
-				}
-				$aResults[$sObjClass][] = $oObj;
-			}
-		}
-
+		$aResults = $oRelGraph->GetObjectsByClass();
 		$oDisplayGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down'));
 		
+		$aGroups = array();
+		$iGroupIdx = 0;
 		$oIterator = new RelationTypeIterator($oDisplayGraph, 'Node');
 		foreach($oIterator as $oNode)
 		{
@@ -1646,12 +1479,14 @@ EOF
 		if ($sFirstTab == 'list')
 		{
 			DisplayNavigatorListTab($oP, $aResults, $sRelation, $oObj);
-			DisplayNavigatorGraphicsTab($oP, $aResults, $oDisplayGraph, $sClass, $id, $sRelation, $oAppContext, ($sDirection == 'down'));
+			$oP->SetCurrentTab(Dict::S('UI:RelationshipGraph'));
+			$oDisplayGraph->Display($oP, $aResults, $sRelation, $oAppContext, ($sDirection == 'down'));
 			DisplayNavigatorGroupTab($oP, $aGroups, $sRelation, $oObj);
 		}
 		else
 		{
-			DisplayNavigatorGraphicsTab($oP, $aResults, $oDisplayGraph, $sClass, $id, $sRelation, $oAppContext, ($sDirection == 'down'));
+			$oP->SetCurrentTab(Dict::S('UI:RelationshipGraph'));
+			$oDisplayGraph->Display($oP, $aResults, $sRelation, $oAppContext, ($sDirection == 'down'));
 			DisplayNavigatorListTab($oP, $aResults, $sRelation, $oObj);
 			DisplayNavigatorGroupTab($oP, $aGroups, $sRelation, $oObj);
 		}

+ 120 - 26
pages/ajax.render.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2014 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -1731,9 +1731,7 @@ EOF
 		require_once(APPROOT.'core/simplegraph.class.inc.php');
 		require_once(APPROOT.'core/relationgraph.class.inc.php');
 		require_once(APPROOT.'core/displayablegraph.class.inc.php');
-		$sClass = utils::ReadParam('class', '', false, 'class');
-		$id = (int)utils::ReadParam('id', 0);
-		$sRelation = utils::ReadParam('relation', 'impact');
+		$sRelation = utils::ReadParam('relation', 'impacts');
 		$sDirection = utils::ReadParam('direction', 'down');
 		
 		$iGroupingThreshold = utils::ReadParam('g', 5);
@@ -1741,7 +1739,7 @@ EOF
 		$sPageOrientation = utils::ReadParam('o', 'L');
 		$sTitle = utils::ReadParam('title', '', false, 'raw_data');
 		$sPositions = utils::ReadParam('positions', null, false, 'raw_data');
-		$aExcluded = utils::ReadParam('excluded', array(), false, 'raw_data');
+		$aExcludedClasses = utils::ReadParam('excluded_classes', array(), false, 'raw_data');
 		$bIncludeList = (bool)utils::ReadParam('include_list', false);
 		$sComments = utils::ReadParam('comments', '', false, 'raw_data');
 		$aPositions = null;
@@ -1750,26 +1748,52 @@ EOF
 			$aPositions = json_decode($sPositions, true);
 		}
 		
-		$oObj = MetaModel::GetObject($sClass, $id);
+		// Get the list of source objects
+		$aSources = utils::ReadParam('sources', array(), false, 'raw_data');
+		$aSourceObjects = array();
+		foreach($aSources as $sClass => $aIDs)
+		{
+			$oSearch = new DBObjectSearch($sClass);
+			$oSearch->AddCondition('id', $aIDs, 'IN');
+			$oSet = new DBObjectSet($oSearch);
+			while($oObj = $oSet->Fetch())
+			{
+				$aSourceObjects[] = $oObj;
+			}
+		}
+		
+		// Get the list of excluded objects
+		$aExcluded = utils::ReadParam('excluded', array(), false, 'raw_data');
+		$aExcludedObjects = array();
+		foreach($aExcluded as $sClass => $aIDs)
+		{
+			$oSearch = new DBObjectSearch($sClass);
+			$oSearch->AddCondition('id', $aIDs, 'IN');
+			$oSet = new DBObjectSet($oSearch);
+			while($oObj = $oSet->Fetch())
+			{
+				$aExcludedObjects[] = $oObj;
+			}
+		}
+		
 		$iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
-		$aSourceObjects = array($oObj);
 		if ($sDirection == 'up')
 		{
 			$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth);
 		}
 		else
 		{
-			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth);
+			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects);
 		}
 		
 		// Remove excluded classes from the graph
-		if (count($aExcluded) > 0)
+		if (count($aExcludedClasses) > 0)
 		{
 			$oIterator = new RelationTypeIterator($oRelGraph, 'Node');
 			foreach($oIterator as $oNode)
 			{
 				$oObj = $oNode->GetProperty('object');
-				if ($oObj && in_array(get_class($oObj), $aExcluded))
+				if ($oObj && in_array(get_class($oObj), $aExcludedClasses))
 				{
 					$oRelGraph->FilterNode($oNode);
 				}
@@ -1798,13 +1822,7 @@ EOF
 		}
 		// First page is the graph
 		$oGraph->RenderAsPDF($oPage, $sComments);
-		/*
-		// Experimental QR code at the bottom left of the page...
-		$sUrl = "r=$sRelation&d=$sDirection&c=$sClass&id=$id";
-		$oPdf = $oPage->get_tcpdf();
-		$aMargins = $oPdf->getMargins();
-		$oPdf->write2DBarcode($sUrl, 'QRCODE,H', $aMargins['left'], $oPdf->getPageHeight() - $aMargins['bottom'] - 35, 25, 25, array(), 'N');
-		*/
+
 		if ($bIncludeList)
 		{
 			// Then the lists of objects (one table per finalclass)
@@ -1864,39 +1882,64 @@ EOF
 		require_once(APPROOT.'core/simplegraph.class.inc.php');
 		require_once(APPROOT.'core/relationgraph.class.inc.php');
 		require_once(APPROOT.'core/displayablegraph.class.inc.php');
-		$sClass = utils::ReadParam('class', '', false, 'class');
-		$id = utils::ReadParam('id', 0);
-		$sRelation = utils::ReadParam('relation', 'impact');
+		$sRelation = utils::ReadParam('relation', 'impacts');
 		$sDirection = utils::ReadParam('direction', 'down');
 		$iGroupingThreshold = utils::ReadParam('g', 5);
 		$sPositions = utils::ReadParam('positions', null, false, 'raw_data');
-		$aExcluded = utils::ReadParam('excluded', array(), false, 'raw_data');
+		$aExcludedClasses = utils::ReadParam('excluded_classes', array(), false, 'raw_data');
 		$aPositions = null;
 		if ($sPositions != null)
 		{
 			$aPositions = json_decode($sPositions, true);
 		}
 		
-		$oObj = MetaModel::GetObject($sClass, $id);
+			// Get the list of source objects
+		$aSources = utils::ReadParam('sources', array(), false, 'raw_data');
+		$aSourceObjects = array();
+		foreach($aSources as $sClass => $aIDs)
+		{
+			$oSearch = new DBObjectSearch($sClass);
+			$oSearch->AddCondition('id', $aIDs, 'IN');
+			$oSet = new DBObjectSet($oSearch);
+			while($oObj = $oSet->Fetch())
+			{
+				$aSourceObjects[] = $oObj;
+			}
+		}
+		
+		// Get the list of excluded objects
+		$aExcluded = utils::ReadParam('excluded', array(), false, 'raw_data');
+		$aExcludedObjects = array();
+		foreach($aExcluded as $sClass => $aIDs)
+		{
+			$oSearch = new DBObjectSearch($sClass);
+			$oSearch->AddCondition('id', $aIDs, 'IN');
+			$oSet = new DBObjectSet($oSearch);
+			while($oObj = $oSet->Fetch())
+			{
+				$aExcludedObjects[] = $oObj;
+			}
+		}
+		
+		// Compute the graph
 		$iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
-		$aSourceObjects = array($oObj);
 		if ($sDirection == 'up')
 		{
 			$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth);
 		}
 		else
 		{
-			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth);
+			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects);
 		}
 		
 		// Remove excluded classes from the graph
-		if (count($aExcluded) > 0)
+		if (count($aExcludedClasses) > 0)
 		{
 			$oIterator = new RelationTypeIterator($oRelGraph, 'Node');
 			foreach($oIterator as $oNode)
 			{
 				$oObj = $oNode->GetProperty('object');
-				if ($oObj && in_array(get_class($oObj), $aExcluded))
+				if ($oObj && in_array(get_class($oObj), $aExcludedClasses))
 				{
 					$oRelGraph->FilterNode($oNode);
 				}
@@ -1913,6 +1956,57 @@ EOF
 		$oPage->SetContentType('application/json');
 		break;
 		
+		case 'ticket_impact':
+		require_once(APPROOT.'core/simplegraph.class.inc.php');
+		require_once(APPROOT.'core/relationgraph.class.inc.php');
+		require_once(APPROOT.'core/displayablegraph.class.inc.php');
+		$sRelation = utils::ReadParam('relation', 'impacts');
+		$sDirection = utils::ReadParam('direction', 'down');
+		$iGroupingThreshold = utils::ReadParam('g', 5);
+		$sClass = utils::ReadParam('class', '', false, 'class');
+		$sAttCode = utils::ReadParam('attcode', 'functionalcis_list');
+		$sImpactAttCode = utils::ReadParam('impact_attcode', 'impact_code');
+		$sImpactAttCodeValue = utils::ReadParam('impact_attcode_value', 'manual');
+		$iId = (int)utils::ReadParam('id', 0, false, 'integer');
+		
+		// Get the list of source objects
+		$oTicket = MetaModel::GetObject($sClass, $iId);
+		$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+		$sExtKeyToRemote = $oAttDef->GetExtKeyToRemote();
+		$oExtKeyToRemote = MetaModel::GetAttributeDef($oAttDef->GetLinkedClass(), $sExtKeyToRemote);
+		$sRemoteClass = $oExtKeyToRemote->GetTargetClass();
+		$oSet = $oTicket->Get($sAttCode);
+		$aSourceObjects = array();
+		$aExcludedObjects = array();
+		while($oLnk = $oSet->Fetch())
+		{
+			if ($oLnk->Get($sImpactAttCode) == 'manual')
+			{
+				$aSourceObjects[] = MetaModel::GetObject($sRemoteClass, $oLnk->Get($sExtKeyToRemote));
+			}
+			if ($oLnk->Get($sImpactAttCode) == 'not_impacted')
+			{
+				$aExcludedObjects[] = MetaModel::GetObject($sRemoteClass, $oLnk->Get($sExtKeyToRemote));
+			}
+		}
+		
+		// Compute the graph
+		$iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
+		if ($sDirection == 'up')
+		{
+			$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth);
+		}
+		else
+		{
+			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, $aExcludedObjects);
+		}
+		
+		$aResults = $oRelGraph->GetObjectsByClass();
+		$oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down'));
+		$oAppContext = new ApplicationContext();
+		$oGraph->Display($oPage, $aResults, $sRelation, $oAppContext, $aExcludedObjects);		
+		break;
+		
 		default:
 		$oPage->p("Invalid query.");
 	}