浏览代码

#328 Added the capability to import/export link sets in CSV format

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@1129 a333f486-631f-4898-b8df-5754b55c2be0
romainq 14 年之前
父节点
当前提交
3446af19e8

+ 7 - 3
application/cmdbabstract.class.inc.php

@@ -827,6 +827,8 @@ 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;
+
 		$aList = array();
 
 		$oAppContext = new ApplicationContext();
@@ -845,7 +847,9 @@ EOF
 		{
 			foreach(MetaModel::ListAttributeDefs($sClassName) as $sAttCode => $oAttDef)
 			{
-				if ((($oAttDef->IsExternalField()) || ($oAttDef->IsWritable())) && $oAttDef->IsScalar())
+				if (!is_null($aFields) && !in_array($sAttCode, $aFields)) continue;
+
+				if ($oAttDef->IsExternalField() || $oAttDef->IsWritable())
 				{
 					$aList[$sClassName][$sAttCode] = $oAttDef;
 				}
@@ -906,7 +910,7 @@ EOF
 					}
 					else
 					{
-						$aRow[] = $oObj->GetAsCSV($sAttCode, $sSeparator, '\\');
+						$aRow[] = $oObj->GetAsCSV($sAttCode, $sSeparator, $sTextQualifier);
 					}
 				}
 			}
@@ -959,7 +963,7 @@ EOF
 					}
 					else
 					{
-						if (($oAttDef->IsWritable()) && ($oAttDef->IsScalar()))
+						if ($oAttDef->IsWritable())
 						{
 							$sValue = $oObj->GetAsXML($sAttCode);
 							$oPage->add("<$sAttCode>$sValue</$sAttCode>\n");

+ 211 - 39
core/attributedef.class.inc.php

@@ -269,17 +269,17 @@ abstract class AttributeDefinition
 		return (string)$sValue;
 	}
 
-	public function GetAsHTML($sValue)
+	public function GetAsHTML($sValue, $oHostObject = null)
 	{
 		return Str::pure2html((string)$sValue);
 	}
 
-	public function GetAsXML($sValue)
+	public function GetAsXML($sValue, $oHostObject = null)
 	{
 		return Str::pure2xml((string)$sValue);
 	}
 
-	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"')
+	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null)
 	{
 		return (string)$sValue;
 	}
@@ -365,21 +365,188 @@ class AttributeLinkedSet extends AttributeDefinition
 	public function GetBasicFilterLooseOperator() {return '';}
 	public function GetBasicFilterSQLExpr($sOpCode, $value) {return '';}
 
-	public function GetAsHTML($sValue)
+	public function GetAsHTML($sValue, $oHostObject = null)
 	{
-		return "ERROR: LIST OF OBJECTS";
+		if (is_object($sValue) && ($sValue instanceof DBObjectSet))
+		{
+			$sValue->Rewind();
+			$aItems = array();
+			while ($oObj = $sValue->Fetch())
+			{
+				// Show only relevant information (hide the external key to the current object)
+				$aAttributes = array();
+				foreach(MetaModel::ListAttributeDefs($this->GetLinkedClass()) as $sAttCode => $oAttDef)
+				{
+					if ($sAttCode == $this->GetExtKeyToMe()) continue;
+					if ($oAttDef->IsExternalField()) continue;
+					$sAttValue = $oObj->GetAsHTML($sAttCode);
+					if (strlen($sAttValue) > 0)
+					{
+						$aAttributes[] = $sAttValue;
+					}
+				}
+				$sAttributes = implode(', ', $aAttributes);
+				$aItems[] = $sAttributes;
+			}
+			return implode('<br/>', $aItems);
+		}
+		return null;
 	}
 
-	public function GetAsXML($sValue)
+	public function GetAsXML($sValue, $oHostObject = null)
 	{
-		return "ERROR: LIST OF OBJECTS";
+		return "Sorry, no yet implemented";
 	}
 
-	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"')
+	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null)
 	{
-		return "ERROR: LIST OF OBJECTS";
+		$sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator');
+		$sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator');
+		$sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator');
+		$sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier');
+
+		if (is_object($sValue) && ($sValue instanceof DBObjectSet))
+		{
+			$sValue->Rewind();
+			$aItems = array();
+			while ($oObj = $sValue->Fetch())
+			{
+				// Show only relevant information (hide the external key to the current object)
+				$aAttributes = array();
+				foreach(MetaModel::ListAttributeDefs($this->GetLinkedClass()) as $sAttCode => $oAttDef)
+				{
+					if ($sAttCode == $this->GetExtKeyToMe()) continue;
+					if ($oAttDef->IsExternalField()) continue;
+					if (!$oAttDef->IsDirectField()) continue;
+					if (!$oAttDef->IsScalar()) continue;
+					$sAttValue = $oObj->GetAsCSV($sAttCode, $sSepValue, '');
+					if (strlen($sAttValue) > 0)
+					{
+						$sAttributeData = str_replace($sAttributeQualifier, $sAttributeQualifier.$sAttributeQualifier, $sAttCode.$sSepValue.$sAttValue);
+						$aAttributes[] = $sAttributeQualifier.$sAttributeData.$sAttributeQualifier;
+					}
+				}
+				$sAttributes = implode($sSepAttribute, $aAttributes);
+				$aItems[] = $sAttributes;
+			}
+			$sRes = implode($sSepItem, $aItems);
+		}
+		else
+		{
+			$sRes = '';
+		}
+		$sRes = str_replace($sTextQualifier, $sTextQualifier.$sTextQualifier, $sRes);
+		$sRes = $sTextQualifier.$sRes.$sTextQualifier;
+		return $sRes;
 	}
+
 	public function DuplicatesAllowed() {return false;} // No duplicates for 1:n links, never
+
+	// Specific to this kind of attribute : transform a string into a value
+	public function MakeValueFromString($sProposedValue, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null)
+	{
+		if (is_null($sSepItem))
+		{
+			$sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator');
+		}
+		if (is_null($sSepAttribute))
+		{
+			$sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator');
+		}
+		if (is_null($sSepValue))
+		{
+			$sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator');
+		}
+		if (is_null($sAttributeQualifier))
+		{
+			$sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier');
+		}
+
+		$sTargetClass = $this->Get('linked_class');
+
+		$sInput = str_replace($sSepItem, "\n", $sProposedValue);
+		$oCSVParser = new CSVParser($sInput, $sSepAttribute, $sAttributeQualifier);
+
+		$aInput = $oCSVParser->ToArray(0 /* do not skip lines */);
+
+		$aLinks = array();
+		foreach($aInput as $aRow)
+		{
+			$aNewRow = array();
+			$oLink = MetaModel::NewObject($sTargetClass);
+			$aExtKeys = array();
+			foreach($aRow as $sCell)
+			{
+				$iSepPos = strpos($sCell, $sSepValue);
+				if ($iSepPos === false)
+				{
+					// Houston...
+					throw new CoreException('Wrong format for link attribute specification', array('value' => $sCell));
+				}
+
+				$sAttCode = trim(substr($sCell, 0, $iSepPos));
+				$sValue = substr($sCell, $iSepPos + strlen($sSepValue));
+
+				if (preg_match('/^(.+)->(.+)$/', $sAttCode, $aMatches))
+				{
+					$sKeyAttCode = $aMatches[1];
+					$sRemoteAttCode = $aMatches[2];
+					$aExtKeys[$sKeyAttCode][$sRemoteAttCode] = $sValue;
+					if (!MetaModel::IsValidAttCode($sTargetClass, $sKeyAttCode))
+					{
+						throw new CoreException('Wrong attribute code for link attribute specification', array('class' => $sTargetClass, 'attcode' => $sKeyAttCode));
+					}
+					$oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode);
+					$sRemoteClass = $oKeyAttDef->GetTargetClass();
+					if (!MetaModel::IsValidAttCode($sRemoteClass, $sRemoteAttCode))
+					{
+						throw new CoreException('Wrong attribute code for link attribute specification', array('class' => $sRemoteClass, 'attcode' => $sRemoteAttCode));
+					}
+				}
+				else
+				{
+					if(!MetaModel::IsValidAttCode($sTargetClass, $sAttCode))
+					{
+						throw new CoreException('Wrong attribute code for link attribute specification', array('class' => $sTargetClass, 'attcode' => $sAttCode));
+					}
+					$oLink->Set($sAttCode, $sValue);
+				}
+			}
+			// Set external keys from search conditions
+			foreach ($aExtKeys as $sKeyAttCode => $aReconciliation)
+			{
+				$oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode);
+				$sKeyClass = $oKeyAttDef->GetTargetClass();
+				$oExtKeyFilter = new CMDBSearchFilter($sKeyClass);
+				$aReconciliationDesc = array();
+				foreach($aReconciliation as $sRemoteAttCode => $sValue)
+				{
+					$oExtKeyFilter->AddCondition($sRemoteAttCode, $sValue, '=');
+					$aReconciliationDesc[] = "$sRemoteAttCode=$sValue";
+				}
+				$oExtKeySet = new CMDBObjectSet($oExtKeyFilter);
+				switch($oExtKeySet->Count())
+				{
+				case 0:
+					$sReconciliationDesc = implode(', ', $aReconciliationDesc);
+					throw new CoreException("Found no match", array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc));
+					break;
+				case 1:
+					$oRemoteObj = $oExtKeySet->Fetch();
+					$oLink->Set($sKeyAttCode, $oRemoteObj->GetKey());
+					break;
+				default:
+					$sReconciliationDesc = implode(', ', $aReconciliationDesc);
+					throw new CoreException("Found several matches", array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc));
+					// Found several matches, ambiguous
+				}
+			}
+
+			$aLinks[] = $oLink;
+		}
+		$oSet = DBObjectSet::FromArray($sTargetClass, $aLinks);
+		return $oSet;
+	}
 }
 
 /**
@@ -821,12 +988,12 @@ class AttributeString extends AttributeDBField
 		return $value;
 	}
 
-	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"')
+	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null)
 	{
 		$sFrom = array("\r\n", $sTextQualifier);
 		$sTo = array("\n", $sTextQualifier.$sTextQualifier);
 		$sEscaped = str_replace($sFrom, $sTo, (string)$sValue);
-		return '"'.$sEscaped.'"';
+		return $sTextQualifier.$sEscaped.$sTextQualifier;
 	}
 }
 
@@ -864,7 +1031,7 @@ class AttributeClass extends AttributeString
 		return $sDefault;
 	}
 
-	public function GetAsHTML($sValue)
+	public function GetAsHTML($sValue, $oHostObject = null)
 	{
 		if (empty($sValue)) return '';
 		return MetaModel::GetName($sValue);
@@ -953,7 +1120,7 @@ class AttributeFinalClass extends AttributeString
 		return $this->m_sValue;
 	}
 
-	public function GetAsHTML($sValue)
+	public function GetAsHTML($sValue, $oHostObject = null)
 	{
 		if (empty($sValue)) return '';
 		return MetaModel::GetName($sValue);
@@ -995,7 +1162,7 @@ class AttributePassword extends AttributeString
 		return array();
 	}
 
-	public function GetAsHTML($sValue)
+	public function GetAsHTML($sValue, $oHostObject = null)
 	{
 		if (strlen($sValue) == 0)
 		{
@@ -1091,7 +1258,7 @@ class AttributeText extends AttributeString
 		return 65535;
 	}
 
-	public function GetAsHTML($sValue)
+	public function GetAsHTML($sValue, $oHostObject = null)
 	{
 		$sValue = parent::GetAsHTML($sValue);
 
@@ -1167,7 +1334,7 @@ class AttributeText extends AttributeString
 		return $sValue;
 	}
 
-	public function GetAsXML($value)
+	public function GetAsXML($value, $oHostObject = null)
 	{
 		return Str::pure2xml($value);
 	}
@@ -1199,7 +1366,7 @@ class AttributeHTML extends AttributeText
 {
 	public function GetEditClass() {return "HTML";}
 
-	public function GetAsHTML($sValue)
+	public function GetAsHTML($sValue, $oHostObject = null)
 	{
 		return $sValue;
 	}
@@ -1218,7 +1385,7 @@ class AttributeEmailAddress extends AttributeString
 		return "^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$";
 	}
 
-	public function GetAsHTML($sValue)
+	public function GetAsHTML($sValue, $oHostObject = null)
 	{
 		if (empty($sValue)) return '';
 		return '<a class="mailto" href="mailto:'.$sValue.'">'.parent::GetAsHTML($sValue).'</a>';
@@ -1275,7 +1442,7 @@ class AttributeTemplateHTML extends AttributeText
 {
 	public function GetEditClass() {return "HTML";}
 
-	public function GetAsHTML($sValue)
+	public function GetAsHTML($sValue, $oHostObject = null)
 	{
 		return $sValue;
 	}
@@ -1289,7 +1456,7 @@ class AttributeTemplateHTML extends AttributeText
  */
 class AttributeWikiText extends AttributeText
 {
-	public function GetAsHTML($value)
+	public function GetAsHTML($value, $oHostObject = null)
 	{
 		// [SELECT xxxx.... [label]] => hyperlink to a result list
 		// {SELECT xxxx.... [label]} => result list displayed inline
@@ -1374,7 +1541,7 @@ class AttributeEnum extends AttributeString
 		return parent::GetBasicFilterSQLExpr($sOpCode, $value);
 	} 
 
-	public function GetAsHTML($sValue)
+	public function GetAsHTML($sValue, $oHostObject = null)
 	{
 		if (is_null($sValue))
 		{
@@ -1581,17 +1748,17 @@ class AttributeDateTime extends AttributeDBField
 		return $value;
 	}
 
-	public function GetAsHTML($value)
+	public function GetAsHTML($value, $oHostObject = null)
 	{
 		return Str::pure2html($value);
 	}
 
-	public function GetAsXML($value)
+	public function GetAsXML($value, $oHostObject = null)
 	{
 		return Str::pure2xml($value);
 	}
 
-	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"')
+	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null)
 	{
 		$sFrom = array("\r\n", $sTextQualifier);
 		$sTo = array("\n", $sTextQualifier.$sTextQualifier);
@@ -1702,7 +1869,7 @@ class AttributeDuration extends AttributeInteger
 		return $value;
 	}
 
-	public function GetAsHTML($value)
+	public function GetAsHTML($value, $oHostObject = null)
 	{
 		return Str::pure2html(self::FormatDuration($value));
 	}
@@ -1788,7 +1955,7 @@ AttributeDate::InitStatics();
  */
 class AttributeDeadline extends AttributeDateTime
 {
-	public function GetAsHTML($value)
+	public function GetAsHTML($value, $oHostObject = null)
 	{
 		$sResult = '';
 		if ($value !== null)
@@ -2144,17 +2311,17 @@ class AttributeExternalField extends AttributeDefinition
 		return $oExtAttDef->FromSQLToValue($aCols, $sPrefix);
 	}
 
-	public function GetAsHTML($value)
+	public function GetAsHTML($value, $oHostObject = null)
 	{
 		$oExtAttDef = $this->GetExtAttDef();
 		return $oExtAttDef->GetAsHTML($value);
 	}
-	public function GetAsXML($value)
+	public function GetAsXML($value, $oHostObject = null)
 	{
 		$oExtAttDef = $this->GetExtAttDef();
 		return $oExtAttDef->GetAsXML($value);
 	}
-	public function GetAsCSV($value, $sSeparator = ',', $sTestQualifier = '"')
+	public function GetAsCSV($value, $sSeparator = ',', $sTestQualifier = '"', $oHostObject = null)
 	{
 		$oExtAttDef = $this->GetExtAttDef();
 		return $oExtAttDef->GetAsCSV($value, $sSeparator, $sTestQualifier);
@@ -2176,7 +2343,7 @@ class AttributeURL extends AttributeString
 
 	public function GetEditClass() {return "String";}
 
-	public function GetAsHTML($sValue)
+	public function GetAsHTML($sValue, $oHostObject = null)
 	{
 		$sTarget = $this->Get("target");
 		if (empty($sTarget)) $sTarget = "_blank";
@@ -2326,7 +2493,7 @@ class AttributeBlob extends AttributeDefinition
 		return 'true';
 	} 
 
-	public function GetAsHTML($value)
+	public function GetAsHTML($value, $oHostObject = null)
 	{
 		if (is_object($value))
 		{
@@ -2334,12 +2501,12 @@ class AttributeBlob extends AttributeDefinition
 		}
 	}
 
-	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"')
+	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null)
 	{
 		return ''; // Not exportable in CSV !
 	}
 	
-	public function GetAsXML($value)
+	public function GetAsXML($value, $oHostObject = null)
 	{
 		return ''; // Not exportable in XML, or as CDATA + some subtags ??
 	}
@@ -2479,7 +2646,7 @@ class AttributeOneWayPassword extends AttributeDefinition
 		return 'true';
 	} 
 
-	public function GetAsHTML($value)
+	public function GetAsHTML($value, $oHostObject = null)
 	{
 		if (is_object($value))
 		{
@@ -2487,12 +2654,12 @@ class AttributeOneWayPassword extends AttributeDefinition
 		}
 	}
 
-	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"')
+	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null)
 	{
 		return ''; // Not exportable in CSV
 	}
 	
-	public function GetAsXML($value)
+	public function GetAsXML($value, $oHostObject = null)
 	{
 		return ''; // Not exportable in XML
 	}
@@ -2544,7 +2711,7 @@ class AttributeTable extends AttributeText
 		return $aValues;
 	}
 
-	public function GetAsHTML($value)
+	public function GetAsHTML($value, $oHostObject = null)
 	{
 		if (!is_array($value))
 		{
@@ -2589,7 +2756,7 @@ class AttributePropertySet extends AttributeTable
 		return $proposedValue;
 	}
 
-	public function GetAsHTML($value)
+	public function GetAsHTML($value, $oHostObject = null)
 	{
 		if (!is_array($value))
 		{
@@ -2737,6 +2904,11 @@ class AttributeFriendlyName extends AttributeComputedFieldVoid
 		return false;
 	}
 
+	public function IsDirectField()
+	{
+		return false;
+	}
+
 	public function SetFixedValue($sValue)
 	{
 		$this->m_sValue = $sValue;
@@ -2746,7 +2918,7 @@ class AttributeFriendlyName extends AttributeComputedFieldVoid
 		return $this->m_sValue;
 	}
 
-	public function GetAsHTML($sValue)
+	public function GetAsHTML($sValue, $oHostObject = null)
 	{
 		return Str::pure2html((string)$sValue);
 	}

+ 60 - 24
core/bulkchange.class.inc.php

@@ -52,19 +52,13 @@ abstract class CellChangeSpec
 		$this->m_sOql = $sOql;
 	}
 
-	static protected function ValueAsHtml($value)
+	public function GetPureValue()
 	{
-		if (MetaModel::IsValidObject($value))
-		{
-			return $value->GetHyperLink();
-		}
-		else
-		{
-			return htmlentities($value, ENT_QUOTES, 'UTF-8');
-		}
+		// Todo - distinguish both values
+		return $this->m_proposedValue;
 	}
 
-	public function GetValue()
+	public function GetDisplayableValue()
 	{
 		return $this->m_proposedValue;
 	}
@@ -101,10 +95,10 @@ class CellStatus_Modify extends CellChangeSpec
 		return 'Modified';
 	}
 
-	public function GetPreviousValue()
-	{
-		return $this->m_previousValue;
-	}
+	//public function GetPreviousValue()
+	//{
+	//	return $this->m_previousValue;
+	//}
 }
 
 class CellStatus_Issue extends CellStatus_Modify
@@ -270,6 +264,22 @@ class BulkChange
 		$this->m_aOnDisappear = $aOnDisappear;
 	}
 
+	protected $m_bReportHtml = false;
+	protected $m_sReportCsvSep = ',';
+	protected $m_sReportCsvDelimiter = '"';
+
+	public function SetReportHtml()
+	{
+		$this->m_bReportHtml = true;
+	}
+
+	public function SetReportCsv($sSeparator = ',', $sDelimiter = '"')
+	{
+		$this->m_bReportHtml = false;
+		$this->m_sReportCsvSep = $sSeparator;
+		$this->m_sReportCsvDelimiter = $sDelimiter;
+	}
+   
 	protected function ResolveExternalKey($aRowData, $sAttCode, &$aResults)
 	{
 		$oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
@@ -388,15 +398,31 @@ class BulkChange
 			// skip the private key, if any
 			if ($sAttCode == 'id') continue;
 
-			$res = $oTargetObj->CheckValue($sAttCode, $aRowData[$iCol]);
-			if ($res === true)
+			$oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
+			if ($oAttDef->IsLinkSet())
 			{
-				$oTargetObj->Set($sAttCode, $aRowData[$iCol]);
+				try
+				{
+					$oSet = $oAttDef->MakeValueFromString($aRowData[$iCol]);
+					$oTargetObj->Set($sAttCode, $oSet);
+				}
+				catch(CoreException $e)
+				{
+					$aErrors[$sAttCode] = "Failed to process input: ".$e->getMessage();
+				}
 			}
 			else
-			{
-				// $res is a string with the error description
-				$aErrors[$sAttCode] = "Unexpected value for attribute '$sAttCode': $res";
+			{ 
+				$res = $oTargetObj->CheckValue($sAttCode, $aRowData[$iCol]);
+				if ($res === true)
+				{
+					$oTargetObj->Set($sAttCode, $aRowData[$iCol]);
+				}
+				else
+				{
+					// $res is a string with the error description
+					$aErrors[$sAttCode] = "Unexpected value for attribute '$sAttCode': $res";
+				}
 			}
 		}
 	
@@ -409,19 +435,29 @@ 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($oTargetObj->Get($sAttCode), $oTargetObj->GetOriginal($sAttCode), $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($oTargetObj->Get($sAttCode));
+					$aResults[$iCol]= new CellStatus_Void($sCurValue);
 				}
 				else
 				{
-					$aResults[$iCol]= new CellStatus_Modify($oTargetObj->Get($sAttCode), $oTargetObj->GetOriginal($sAttCode));
+					$aResults[$iCol]= new CellStatus_Modify($sCurValue, $sOrigValue);
 				}
 			}
 			else
@@ -970,7 +1006,7 @@ EOF
 			{
 				$aObjects[$iObjId]['__created__'] = true;
 			}
-			elseif (is_subclass_of($oOperation, 'CMDBChangeOpSetAttribute'))
+			elseif ($oOperation instanceof CMDBChangeOpSetAttribute)
 			{
 				$sAttCode = $oOperation->Get('attcode');
 

+ 32 - 0
core/config.class.inc.php

@@ -221,6 +221,38 @@ class Config
 			'source_of_value' => '',
 			'show_in_conf_sample' => true,
 		),
+		'link_set_item_separator' => array(
+			'type' => 'string',
+			'description' => 'Link set from string: line separator',
+			'default' => '|',
+			'value' => '|',
+			'source_of_value' => '',
+			'show_in_conf_sample' => true,
+		),
+		'link_set_attribute_separator' => array(
+			'type' => 'string',
+			'description' => 'Link set from string: attribute separator',
+			'default' => ';',
+			'value' => ';',
+			'source_of_value' => '',
+			'show_in_conf_sample' => true,
+		),
+		'link_set_value_separator' => array(
+			'type' => 'string',
+			'description' => 'Link set from string: value separator (between the attcode and the value itself',
+			'default' => ':',
+			'value' => ':',
+			'source_of_value' => '',
+			'show_in_conf_sample' => true,
+		),
+		'link_set_attribute_qualifier' => array(
+			'type' => 'string',
+			'description' => 'Link set from string: attribute qualifier (encloses both the attcode and the value)',
+			'default' => "'",
+			'value' => "'",
+			'source_of_value' => '',
+			'show_in_conf_sample' => true,
+		),
 	);
 
 	public function IsProperty($sPropCode)

+ 1 - 0
core/csvparser.class.inc.php

@@ -184,6 +184,7 @@ class CSVParser
 		{
 			if ($i == $iDataLength)
 			{
+				$c = null;
 				$iEvent = evEND;
 			}
 			else

+ 20 - 2
core/dbobject.class.php

@@ -481,13 +481,31 @@ abstract class DBObject
 	public function GetAsXML($sAttCode)
 	{
 		$oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
-		return $oAtt->GetAsXML($this->Get($sAttCode));
+		return $oAtt->GetAsXML($this->Get($sAttCode), $this);
 	}
 
 	public function GetAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"')
 	{
 		$oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
-		return $oAtt->GetAsCSV($this->Get($sAttCode), $sSeparator, $sTextQualifier);
+		return $oAtt->GetAsCSV($this->Get($sAttCode), $sSeparator, $sTextQualifier, $this);
+	}
+
+	public function GetOriginalAsHTML($sAttCode)
+	{
+		$oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
+		return $oAtt->GetAsHTML($this->GetOriginal($sAttCode), $this);
+	}
+
+	public function GetOriginalAsXML($sAttCode)
+	{
+		$oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
+		return $oAtt->GetAsXML($this->GetOriginal($sAttCode), $this);
+	}
+
+	public function GetOriginalAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"')
+	{
+		$oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
+		return $oAtt->GetAsCSV($this->GetOriginal($sAttCode), $sSeparator, $sTextQualifier, $this);
 	}
 
 	protected static function MakeHyperLink($sObjClass, $sObjKey, $sLabel = '')

+ 2 - 1
pages/ajax.csvimport.php

@@ -163,8 +163,9 @@ function GetMappingForField($sClassName, $sFieldName, $iFieldIndex, $bAdvancedMo
 				}
 			}
 		}
-		else if ( ($oAttDef->IsWritable()) && (!$oAttDef->IsLinkSet()) )
+		else if ($oAttDef->IsWritable() && ($bAdvancedMode || !$oAttDef->IsLinkset()))
 		{
+			
 			if (!$oAttDef->IsNullAllowed())
 			{
 				$sStar = '*';

+ 7 - 6
pages/csvimport.php

@@ -315,7 +315,8 @@ try
 			empty($sSynchroScope) ? null : $sSynchroScope,
 			$aSynchroUpdate		
 		);
-		
+		$oBulk->SetReportHtml();
+
 		$oPage->add('<input type="hidden" name="csvdata_truncated" id="csvdata_truncated" value="'.htmlentities($sCSVDataTruncated, ENT_QUOTES, 'UTF-8').'"/>');
 		$aRes = $oBulk->Process($oMyChange);
 
@@ -350,7 +351,7 @@ try
 				case 'RowStatus_NoChange':
 				$iUnchanged++;
 				$sFinalClass = $aResRow['finalclass'];
-				$oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetValue());
+				$oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue());
 				$sUrl = $oObj->GetHyperlink();
 				$sStatus = '<img src="../images/unchanged.png" title="Unchanged">';
 				$sCSSRowClass = 'row_unchanged';
@@ -359,7 +360,7 @@ try
 				case 'RowStatus_Modify':
 				$iModified++;
 				$sFinalClass = $aResRow['finalclass'];
-				$oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetValue());
+				$oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue());
 				$sUrl = $oObj->GetHyperlink();
 				$sStatus = '<img src="../images/modified.png" title="Modified">';
 				$sCSSRowClass = 'row_modified';
@@ -368,7 +369,7 @@ try
 				case 'RowStatus_Disappeared':
 				$iModified++;
 				$sFinalClass = $aResRow['finalclass'];
-				$oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetValue());
+				$oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue());
 				$sUrl = $oObj->GetHyperlink();
 				$sStatus = '<img src="../images/delete.png" title="Missing">';
 				$sCSSRowClass = 'row_modified';
@@ -394,7 +395,7 @@ try
 				else
 				{
 					$sFinalClass = $aResRow['finalclass'];
-					$oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetValue());
+					$oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue());
 					$sUrl = $oObj->GetHyperlink();
 					$sMessage = 'Object created';				
 				}
@@ -443,7 +444,7 @@ try
 							// Do nothing
 						}
 					}
-					$sHtmlValue = htmlentities($oCellStatus->GetValue(), ENT_QUOTES, 'UTF-8');
+					$sHtmlValue = $oCellStatus->GetDisplayableValue();
 					switch(get_class($oCellStatus))
 					{
 						case 'CellStatus_Issue':

+ 75 - 10
test/testlist.inc.php

@@ -1326,26 +1326,45 @@ class TestItopBulkLoad extends TestBizModel
 	
 	protected function DoExecute()
 	{
-		$oParser = new CSVParser("name,org_id->name,brand,model
-		Server1,Demo,,
-		server4,Demo,,
+		$sLogin = 'testbulkload_'.time();
+
+		$oParser = new CSVParser("login,contactid->name,password,profile_list
+		_1_$sLogin,Picasso,secret1,profileid:10;reason:service manager|profileid->name:Problem Manager;'reason:toto;problem manager'
+		_2_$sLogin,Picasso,secret2,
 		", ',', '"');
-		$aData = $oParser->ToArray(1, array('_name', '_org_name', '_brand', '_model'));
+		$aData = $oParser->ToArray(1, array('_login', '_contact_name', '_password', '_profiles'));
 		self::DumpVariable($aData);
 
+		$oUser = new UserLocal();
+		$oUser->Set('login', 'patator');
+		$oUser->Set('password', 'patator');
+		//$oUser->Set('contactid', 0);
+		//$oUser->Set('language', $sLanguage);
+
+		$aProfiles = array(
+			array(
+				'profileid' => 10, // Service Manager
+				'reason' => 'service manager',
+			),
+			array(
+				'profileid->name' => 'Problem Manager',
+				'reason' => 'problem manager',
+			),
+		);
+
 		$oBulk = new BulkChange(
-			'Server',
+			'UserLocal',
 			$aData,
 			// attributes
-			array('name' => '_name', 'brand' => '_brand', 'model' => '_model'),
+			array('login' => '_login', 'password' => '_password', 'profile_list' => '_profiles'),
 			// ext keys
-			array('org_id' => array('name' => '_org_name')),
+			array('contactid' => array('name' => '_contact_name')),
 			// reconciliation
-			array('name'),
+			array('login'),
 			// Synchro - scope
-			"SELECT Server",
+			"SELECT UserLocal",
 			// Synchro - set attribute on missing objects
-			array ('brand' => 'you let package', 'model' => 'tpe', 'cpu' => 'it is pay you')
+			array ('password' => 'terminated', 'login' => 'terminated'.time())
 		);
 
 		if (false)
@@ -1786,6 +1805,19 @@ class TestImportREST extends TestWebServices
 				),
 				'csvdata' => "org_name;name;address\nDemo;Le pantheon;restore address?",
 			),
+			array(
+				'desc' => 'Load a user account',
+				'login' => 'admin',
+				'password' => 'admin',
+				'args' => array(
+					'class' => 'UserLocal',
+					'output' => 'details',
+					'separator' => ',',
+					'simulate' => '0',
+					'comment' => 'automated testing'
+				),
+				'csvdata' => "login,password,profile_list\nby_import_csv,fakepwd,profileid->name:Configuration Manager|profileid:10;reason:direct id",
+			),
 		); 
 
      	$sSubTests = utils::ReadParam('subtests', null);
@@ -3105,5 +3137,38 @@ class TestCreateObjects extends TestBizModel
 	}
 }
 
+class TestSetLinkset extends TestBizModel
+{
+	static public function GetName()
+	{
+		return 'Itop - Link set from a string';
+	}
+
+	static public function GetDescription()
+	{
+		return 'Create a user account, setting its profile by the mean of a string (prerequisite to CSV import of linksets)';
+	}
+
+	static public function GetConfigFile() {return '/config-itop.php';}
+
+	protected function DoExecute()
+	{
+		$oUser = new UserLocal();
+		$oUser->Set('login', 'patator'.time());
+		$oUser->Set('password', 'patator');
+		//$oUser->Set('contactid', 0);
+		//$oUser->Set('language', $sLanguage);
+
+      $sLinkSetSpec = "profileid:10;reason:service manager|profileid->name:Problem Manager;'reason:problem manager;glandeur";
+
+		$oAttDef = MetaModel::GetAttributeDef('UserLocal', 'profile_list');
+		$oSet = $oAttDef->MakeValueFromString($sLinkSetSpec);
+		$oUser->Set('profile_list', $oSet);
+
+		// Create a change to record the history of the User object
+		$this->ObjectToDB($oUser, $bReload = true);
+		echo "<p>Created: {$oUser->GetHyperLink()}</p>";
+	}
+}
 
 ?>

+ 3 - 1
webservices/export.php

@@ -42,6 +42,8 @@ $currentOrganization = utils::ReadParam('org_id', '');
 // Main program
 $sExpression = utils::ReadParam('expression', '');
 $sFormat = strtolower(utils::ReadParam('format', 'html'));
+$sFields = utils::ReadParam('fields', ''); // CSV field list
+
 $oP = null;
 
 if (!empty($sExpression))
@@ -77,7 +79,7 @@ if (!empty($sExpression))
 				
 				case 'csv':
 				$oP = new CSVPage("iTop - Export");
-				cmdbAbstractObject::DisplaySetAsCSV($oP, $oSet);
+				cmdbAbstractObject::DisplaySetAsCSV($oP, $oSet, array('fields' => $sFields));
 				break;
 				
 				case 'xml':

+ 4 - 4
webservices/import.php

@@ -384,7 +384,7 @@ try
 			// Ignore any trailing "star" (*) that simply indicates a mandatory field
 			$sFieldName = $aMatches[1];
 		}
-		if (preg_match('/^(.*)->(.*)$/', trim($sFieldName), $aMatches))
+		if (preg_match('/^(.+)->(.+)$/', trim($sFieldName), $aMatches))
 		{
 			// The column has been specified as "extkey->attcode"
 			//
@@ -472,7 +472,7 @@ try
 			throw new BulkLoadException("Reconciliation keys not found in the input columns '$sReconcKey' (class: '$sClass')");
 		}
 
-		if (preg_match('/^(.*)->(.*)$/', trim($sReconcKey), $aMatches))
+		if (preg_match('/^(.+)->(.+)$/', trim($sReconcKey), $aMatches))
 		{
 			// The column has been specified as "extkey->attcode"
 			//
@@ -697,7 +697,7 @@ try
 			if (isset($aRowData["finalclass"]) && isset($aRowData["id"]))
 			{
 				$aRowDisp["__OBJECT_CLASS__"] = $aRowData["finalclass"];
-				$aRowDisp["__OBJECT_ID__"] = $aRowData["id"]->GetValue();
+				$aRowDisp["__OBJECT_ID__"] = $aRowData["id"]->GetDisplayableValue();
 			}
 			else
 			{
@@ -714,7 +714,7 @@ try
 	
 				if (is_object($value))
 				{
-					$aRowDisp["$sKey"] = $value->GetValue().$value->GetDescription();
+					$aRowDisp["$sKey"] = $value->GetDisplayableValue().$value->GetDescription();
 				}
 				else
 				{