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

SVN:trunk[1130]
This commit is contained in:
Romain Quetiez
2011-03-15 12:19:04 +00:00
parent f3cc490295
commit 7989cf622f
9 changed files with 263 additions and 58 deletions

View File

@@ -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,13 +851,23 @@ 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
{
// User defined list of attributes
if (in_array($sAttCode, $aFields))
{
$aList[$sClassName][$sAttCode] = $oAttDef;
}
}
}
$aHeader[] = 'id';
foreach($aList[$sClassName] as $sAttCode => $oAttDef)
{

View File

@@ -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));
}
$aValues[$sAttCode] = $sValue;
}
}
// 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);
}
}
// Set external keys from search conditions
// 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);
}
}
/**

View File

@@ -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,6 +435,8 @@ class BulkChange
{
$aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]);
}
else
{
if ($this->m_bReportHtml)
{
$sCurValue = $oTargetObj->GetAsHTML($sAttCode);
@@ -466,6 +468,7 @@ class BulkChange
$aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]);
}
}
}
// Checks
//

View File

@@ -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;
}
@@ -852,17 +852,59 @@ abstract class DBObject
// List the attributes that have been changed
// Returns an array of attname => currentvalue
public function ListChanges()
{
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()
{

View File

@@ -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())

View File

@@ -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())

View File

@@ -1101,8 +1101,10 @@ 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);
if ($iMaxLen > 0)
{
$iMaxLines = 20;
$iCurPos = true;
while ( ($iCurPos > 0) && ($iMaxLines > 0))
{
@@ -1119,6 +1121,11 @@ EOF
$iMaxLines--;
}
$sCSVDataTruncated = substr($sUTF8Data, 0, $iCurPos);
}
else
{
$sCSVDataTruncated = '';
}
$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'));

View File

@@ -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,

View File

@@ -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;