Ver Fonte

CSV import: fixed a few issues (restricted to N-N links) + added arguments to the export page, to allow for exporting N-N links

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@1130 a333f486-631f-4898-b8df-5754b55c2be0
romainq há 14 anos atrás
pai
commit
2b5b0351a6

+ 19 - 5
application/cmdbabstract.class.inc.php

@@ -827,7 +827,11 @@ EOF
 	{
 		$sSeparator = isset($aParams['separator']) ? $aParams['separator'] : ','; // default separator is comma
 		$sTextQualifier = isset($aParams['text_qualifier']) ? $aParams['text_qualifier'] : '"'; // default text qualifier is double quote
-		$aFields = isset($aParams['fields']) ? explode(',', $aParams['fields']) : null;
+		$aFields = null;
+		if (isset($aParams['fields']) && (strlen($aParams['fields']) > 0))
+		{
+			$aFields = explode(',', $aParams['fields']);
+		}
 
 		$aList = array();
 
@@ -847,11 +851,21 @@ EOF
 		{
 			foreach(MetaModel::ListAttributeDefs($sClassName) as $sAttCode => $oAttDef)
 			{
-				if (!is_null($aFields) && !in_array($sAttCode, $aFields)) continue;
-
-				if ($oAttDef->IsExternalField() || $oAttDef->IsWritable())
+				if (is_null($aFields) || (count($aFields) == 0))
+				{
+					// Standard list of attributes (no link sets)
+					if ($oAttDef->IsScalar() && ($oAttDef->IsWritable() || $oAttDef->IsExternalField()))
+					{
+						$aList[$sClassName][$sAttCode] = $oAttDef;
+					}
+				}
+				else
 				{
-					$aList[$sClassName][$sAttCode] = $oAttDef;
+					// User defined list of attributes
+					if (in_array($sAttCode, $aFields))
+					{
+						$aList[$sClassName][$sAttCode] = $oAttDef;
+					}
 				}
 			}
 			$aHeader[] = 'id';

+ 59 - 5
core/attributedef.class.inc.php

@@ -203,6 +203,7 @@ abstract class AttributeDefinition
 	public function IsNull($proposedValue) {return is_null($proposedValue);} 
 
 	public function MakeRealValue($proposedValue) {return $proposedValue;} // force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing!)
+	public function Equals($val1, $val2) {return ($val1 == $val2);}
 
 	public function GetSQLExpressions($sPrefix = '') {return array();} // returns suffix/expression pairs (1 in most of the cases), for READING (Select)
 	public function FromSQLToValue($aCols, $sPrefix = '') {return null;} // returns a value out of suffix/value pairs, for SELECT result interpretation
@@ -411,10 +412,19 @@ class AttributeLinkedSet extends AttributeDefinition
 			$aItems = array();
 			while ($oObj = $sValue->Fetch())
 			{
+				$sObjClass = get_class($oObj);
 				// Show only relevant information (hide the external key to the current object)
 				$aAttributes = array();
-				foreach(MetaModel::ListAttributeDefs($this->GetLinkedClass()) as $sAttCode => $oAttDef)
+				foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef)
 				{
+					if ($sAttCode == 'finalclass')
+					{
+						if ($sObjClass == $this->GetLinkedClass())
+						{
+							// Simplify the output if the exact class could be determined implicitely 
+							continue;
+						}
+					}
 					if ($sAttCode == $this->GetExtKeyToMe()) continue;
 					if ($oAttDef->IsExternalField()) continue;
 					if (!$oAttDef->IsDirectField()) continue;
@@ -472,9 +482,9 @@ class AttributeLinkedSet extends AttributeDefinition
 		$aLinks = array();
 		foreach($aInput as $aRow)
 		{
-			$aNewRow = array();
-			$oLink = MetaModel::NewObject($sTargetClass);
+			// 1st - get the values, split the extkey->searchkey specs, and eventually get the finalclass value
 			$aExtKeys = array();
+			$aValues = array();
 			foreach($aRow as $sCell)
 			{
 				$iSepPos = strpos($sCell, $sSepValue);
@@ -509,10 +519,35 @@ class AttributeLinkedSet extends AttributeDefinition
 					{
 						throw new CoreException('Wrong attribute code for link attribute specification', array('class' => $sTargetClass, 'attcode' => $sAttCode));
 					}
-					$oLink->Set($sAttCode, $sValue);
+					$aValues[$sAttCode] = $sValue;
 				}
 			}
-			// Set external keys from search conditions
+
+			// 2nd - Instanciate the object and set the value
+			if (isset($aValues['finalclass']))
+			{
+				$sLinkClass = $aValues['finalclass'];
+				if (!is_subclass_of($sLinkClass, $sTargetClass))
+				{
+					throw new CoreException('Wrong class for link attribute specification', array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass));
+				}
+			}
+			elseif (MetaModel::IsAbstract($sTargetClass))
+			{
+					throw new CoreException('Missing finalclass for link attribute specification');
+			}
+			else
+			{
+				$sLinkClass = $sTargetClass;
+			}
+
+			$oLink = MetaModel::NewObject($sLinkClass);
+			foreach ($aValues as $sAttCode => $sValue)
+			{
+				$oLink->Set($sAttCode, $sValue);
+			}
+
+			// 3rd - Set external keys from search conditions
 			foreach ($aExtKeys as $sKeyAttCode => $aReconciliation)
 			{
 				$oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode);
@@ -547,6 +582,25 @@ class AttributeLinkedSet extends AttributeDefinition
 		$oSet = DBObjectSet::FromArray($sTargetClass, $aLinks);
 		return $oSet;
 	}
+
+	public function Equals($val1, $val2)
+	{
+		if ($val1 === $val2) return true;
+
+		if (is_object($val1) != is_object($val2))
+		{
+			return false;
+		}
+		if (!is_object($val1))
+		{
+			// string ?
+			// todo = implement this case ?
+			return false;
+		}
+
+		// Both values are Object sets
+		return $val1->HasSameContents($val2);
+	}
 }
 
 /**

+ 26 - 23
core/bulkchange.class.inc.php

@@ -399,7 +399,7 @@ class BulkChange
 			if ($sAttCode == 'id') continue;
 
 			$oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
-			if ($oAttDef->IsLinkSet())
+			if ($oAttDef->IsLinkSet() && $oAttDef->IsIndirect())
 			{
 				try
 				{
@@ -435,35 +435,38 @@ class BulkChange
 			{
 				$aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]);
 			}
-			if ($this->m_bReportHtml)
-			{
-				$sCurValue = $oTargetObj->GetAsHTML($sAttCode);
-				$sOrigValue = $oTargetObj->GetOriginalAsHTML($sAttCode);
-			}
 			else
 			{
-				$sCurValue = $oTargetObj->GetAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter);
-				$sOrigValue = $oTargetObj->GetOriginalAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter);
-			}
-			if (isset($aErrors[$sAttCode]))
-			{
-				$aResults[$iCol]= new CellStatus_Issue($sCurValue, $sOrigValue, $aErrors[$sAttCode]);
-			}
-			elseif (array_key_exists($sAttCode, $aChangedFields))
-			{
-				if ($oTargetObj->IsNew())
+				if ($this->m_bReportHtml)
 				{
-					$aResults[$iCol]= new CellStatus_Void($sCurValue);
+					$sCurValue = $oTargetObj->GetAsHTML($sAttCode);
+					$sOrigValue = $oTargetObj->GetOriginalAsHTML($sAttCode);
 				}
 				else
 				{
-					$aResults[$iCol]= new CellStatus_Modify($sCurValue, $sOrigValue);
+					$sCurValue = $oTargetObj->GetAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter);
+					$sOrigValue = $oTargetObj->GetOriginalAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter);
+				}
+				if (isset($aErrors[$sAttCode]))
+				{
+					$aResults[$iCol]= new CellStatus_Issue($sCurValue, $sOrigValue, $aErrors[$sAttCode]);
+				}
+				elseif (array_key_exists($sAttCode, $aChangedFields))
+				{
+					if ($oTargetObj->IsNew())
+					{
+						$aResults[$iCol]= new CellStatus_Void($sCurValue);
+					}
+					else
+					{
+						$aResults[$iCol]= new CellStatus_Modify($sCurValue, $sOrigValue);
+					}
+				}
+				else
+				{
+					// By default... nothing happens
+					$aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]);
 				}
-			}
-			else
-			{
-				// By default... nothing happens
-				$aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]);
 			}
 		}
 	

+ 46 - 4
core/dbobject.class.php

@@ -826,9 +826,9 @@ abstract class DBObject
 			}
 			elseif(is_object($proposedValue))
 			{
+				$oLinkAttDef = MetaModel::GetAttributeDef(get_class($this), $sAtt);
 				// The value is an object, the comparison is not strict
-				// #@# todo - should be even less strict => add verb on AttributeDefinition: Compare($a, $b)
-				if ($this->m_aOrigValues[$sAtt] != $proposedValue)
+				if (!$oLinkAttDef->Equals($proposedValue, $this->m_aOrigValues[$sAtt]))
 				{
 					$aDelta[$sAtt] = $proposedValue;
 				}
@@ -853,16 +853,58 @@ abstract class DBObject
 	// Returns an array of attname => currentvalue
 	public function ListChanges()
 	{
-		return $this->ListChangedValues($this->m_aCurrValues);
+		if ($this->m_bIsInDB)
+		{
+			return $this->ListChangedValues($this->m_aCurrValues);
+		}
+		else
+		{
+			return $this->m_aCurrValues;
+		}
 	}
 
-	// Tells whether or not an object was modified
+	// Tells whether or not an object was modified since last read (ie: does it differ from the DB ?)
 	public function IsModified()
 	{
 		$aChanges = $this->ListChanges();
 		return (count($aChanges) != 0);
 	}
 
+	public function Equals($oSibling)
+	{
+		if (get_class($oSibling) != get_class($this))
+		{
+			return false;
+		}
+		if ($this->GetKey() != $oSibling->GetKey())
+		{
+			return false;
+		}
+		if ($this->m_bIsInDB)
+		{
+			// If one has changed, then consider them as being different
+			if ($this->IsModified() || $oSibling->IsModified())
+			{
+				return false;
+			}
+		}
+		else
+		{
+			// Todo - implement this case (loop on every attribute)
+			//foreach(MetaModel::ListAttributeDefs(get_class($this) as $sAttCode => $oAttDef)
+			//{
+					//if (!isset($this->m_CurrentValues[$sAttCode])) continue;
+					//if (!isset($this->m_CurrentValues[$sAttCode])) continue;
+					//if (!$oAttDef->Equals($this->m_CurrentValues[$sAttCode], $oSibling->m_CurrentValues[$sAttCode]))
+					//{
+						//return false;
+					//}
+			//}
+			return false;
+		}
+		return true;
+	}
+
 	// used both by insert/update
 	private function DBWriteLinks()
 	{

+ 38 - 1
core/dbobjectset.class.php

@@ -49,7 +49,7 @@ class DBObjectSet
 
 		$this->m_bLoaded = false; // true when the filter has been used OR the set is built step by step (AddObject...)
 		$this->m_aData = array(); // array of (row => array of (classalias) => object/null)
-		$this->m_aId2Row = array();
+		$this->m_aId2Row = array(); // array of (pkey => index in m_aData)
 		$this->m_iCurrRow = 0;
 	}
 
@@ -445,6 +445,43 @@ class DBObjectSet
 		return $oNewSet;
 	}
 
+	// Note: This verb works only with objects existing in the database
+	//
+	public function HasSameContents($oObjectSet)
+	{
+		if ($this->GetRootClass() != $oObjectSet->GetRootClass())
+		{
+			return false;
+		}
+		if (!$this->m_bLoaded) $this->Load();
+
+		if ($this->Count() != $oObjectSet->Count())
+		{
+			return false;
+		}
+		$sClassAlias = $this->m_oFilter->GetClassAlias();
+		$oObjectSet->Rewind();
+		while ($oObject = $oObjectSet->Fetch())
+		{
+			$iObjectKey = $oObject->GetKey();
+			if ($iObjectKey < 0)
+			{
+				return false;
+			}
+			if (!array_key_exists($iObjectKey, $this->m_aId2Row[$sClassAlias]))
+			{
+				return false;
+			}
+			$iRow = $this->m_aId2Row[$sClassAlias][$iObjectKey];
+			$oSibling = $this->m_aData[$iRow][$sClassAlias];
+			if (!$oObject->Equals($oSibling))
+			{
+				return false;
+			}
+		}
+		return true;
+	}
+
 	public function CreateDelta($oObjectSet)
 	{
 		if ($this->GetRootClass() != $oObjectSet->GetRootClass())

+ 1 - 1
pages/ajax.csvimport.php

@@ -163,7 +163,7 @@ function GetMappingForField($sClassName, $sFieldName, $iFieldIndex, $bAdvancedMo
 				}
 			}
 		}
-		else if ($oAttDef->IsWritable() && ($bAdvancedMode || !$oAttDef->IsLinkset()))
+		else if ($oAttDef->IsWritable() && (!$oAttDef->IsLinkset() || ($bAdvancedMode && $oAttDef->IsIndirect())))
 		{
 			
 			if (!$oAttDef->IsNullAllowed())

+ 22 - 15
pages/csvimport.php

@@ -1101,24 +1101,31 @@ EOF
 		
 		// Create a truncated version of the data used for the fast preview
 		// Take about 20 lines of data... knowing that some lines may contain carriage returns
-		$iMaxLines = 20;
 		$iMaxLen = strlen($sUTF8Data);
-		$iCurPos = true;
-		while ( ($iCurPos > 0) && ($iMaxLines > 0))
+		if ($iMaxLen > 0)
 		{
-			$pos = strpos($sUTF8Data, "\n", $iCurPos);
-			if ($pos !== false)
+			$iMaxLines = 20;
+			$iCurPos = true;
+			while ( ($iCurPos > 0) && ($iMaxLines > 0))
 			{
-				$iCurPos = 1+$pos;
-			}
-			else
-			{
-				$iCurPos = strlen($sUTF8Data);
-				$iMaxLines = 1;
+				$pos = strpos($sUTF8Data, "\n", $iCurPos);
+				if ($pos !== false)
+				{
+					$iCurPos = 1+$pos;
+				}
+				else
+				{
+					$iCurPos = strlen($sUTF8Data);
+					$iMaxLines = 1;
+				}
+				$iMaxLines--;
 			}
-			$iMaxLines--;
+			$sCSVDataTruncated = substr($sUTF8Data, 0, $iCurPos);
+		}
+		else
+		{
+			$sCSVDataTruncated = '';
 		}
-		$sCSVDataTruncated = substr($sUTF8Data, 0, $iCurPos);
 
 		$sSynchroScope = utils::ReadParam('synchro_scope', '');
 		if (!empty($sSynchroScope))
@@ -1433,7 +1440,7 @@ EOF
 	
 	$oPage->output();
 }
-catch(CoreException $e)
+catch(xxxxxxxCoreException $e)
 {
 	require_once(APPROOT.'/setup/setuppage.class.inc.php');
 	$oP = new SetupWebPage(Dict::S('UI:PageTitle:FatalError'));
@@ -1462,7 +1469,7 @@ catch(CoreException $e)
 	// For debugging only
 	//throw $e;
 }
-catch(Exception $e)
+catch(xxxxxException $e)
 {
 	require_once(APPROOT.'/setup/setuppage.class.inc.php');
 	$oP = new SetupWebPage(Dict::S('UI:PageTitle:FatalError'));

+ 51 - 3
test/testlist.inc.php

@@ -2139,6 +2139,54 @@ class TestDataExchange extends TestBizModel
 	
 	protected function DoExecute()
 	{
+/*
+		$aScenarios = array(
+			array(
+				'desc' => 'Load user logins',
+				'login' => 'admin',
+				'password' => 'admin',
+				'target_class' => 'UserLocal',
+				'full_load_periodicity' => 3600, // should be ignored in this case
+				'reconciliation_policy' => 'use_attributes',
+				'action_on_zero' => 'create',
+				'action_on_one' => 'update',
+				'action_on_multiple' => 'error',
+				'delete_policy' => 'delete',
+				'delete_policy_update' => '',
+				'delete_policy_retention' => 0,
+				'source_data' => array(
+					array('primary_key', 'login', 'password', 'profile_list'),
+					array(
+						array('user_A', 'login_A', 'password_A', 'profileid:10;reason:he/she is managing services'),
+					),
+				),
+				'target_data' => array(
+					array('login'),
+					array(
+						// Initial state
+					),
+					array(
+						array('login_A'),
+					),
+				),
+				'attributes' => array(
+					'login' => array(
+						'do_reconcile' => true,
+						'do_update' => true,
+						'automatic_prefix' => true, // unique id (for unit testing)
+					),
+					'password' => array(
+						'do_reconcile' => false,
+						'do_update' => true,
+					),
+					'profile_list' => array(
+						'do_reconcile' => false,
+						'do_update' => true,
+					),
+				),
+			),
+		);
+*/
 		$aScenarios = array(
 			array(
 				'desc' => 'Simple scenario with delete option (and extkey given as org/name)',
@@ -2222,11 +2270,11 @@ class TestDataExchange extends TestBizModel
 				'action_on_multiple' => 'error',
 				'delete_policy' => 'update_then_delete',
 				'delete_policy_update' => 'status:obsolete',
-				'delete_policy_retention' => 5,
+				'delete_policy_retention' => 15,
 				'source_data' => array(
 					array('primary_key', 'org_id', 'name', 'status'),
 					array(
-						array('obj_A', 'OMED', 'obj_A', 'production'),
+						array('obj_A', 'Demo', 'obj_A', 'production'),
 					),
 					array(
 					),
@@ -2248,7 +2296,7 @@ class TestDataExchange extends TestBizModel
 					'org_id' => array(
 						'do_reconcile' => true,
 						'do_update' => true,
-						'reconciliation_attcode' => 'code',
+						'reconciliation_attcode' => 'name',
 					),
 					'name' => array(
 						'do_reconcile' => true,

+ 1 - 1
webservices/export.php

@@ -42,7 +42,7 @@ $currentOrganization = utils::ReadParam('org_id', '');
 // Main program
 $sExpression = utils::ReadParam('expression', '');
 $sFormat = strtolower(utils::ReadParam('format', 'html'));
-$sFields = utils::ReadParam('fields', ''); // CSV field list
+$sFields = utils::ReadParam('fields', ''); // CSV field list (allows to specify link set attributes, still not taken into account for XML export)
 
 $oP = null;