diff --git a/core/bulkchange.class.inc.php b/core/bulkchange.class.inc.php index f68d227a6..f4f7fbb3b 100644 --- a/core/bulkchange.class.inc.php +++ b/core/bulkchange.class.inc.php @@ -11,7 +11,7 @@ define('UTF8_BOM', chr(239).chr(187).chr(191)); // 0xEF, 0xBB, 0xBF /** * CellChangeSpec - * A series of classes, keeping the information about a given cell: could it be changed or not (and why)? + * A series of classes, keeping the information about a given cell: could it be changed or not (and why)? * * @package iTopORM */ @@ -42,6 +42,17 @@ abstract class CellChangeSpec return $this->m_sOql; } + /** + * @since 3.1.0 N°5305 + */ + public function GetDisplayableValueAndDescription(): string + { + return sprintf("%s%s", + $this->GetDisplayableValue(), + $this->GetDescription() + ); + } + abstract public function GetDescription(); } @@ -86,26 +97,90 @@ class CellStatus_Issue extends CellStatus_Modify parent::__construct($proposedValue, $previousValue); } - public function GetDescription() + public function GetDisplayableValue() { if (is_null($this->m_proposedValue)) { - return Dict::Format('UI:CSVReport-Value-SetIssue', $this->m_sReason); + return Dict::Format('UI:CSVReport-Value-SetIssue'); } - return Dict::Format('UI:CSVReport-Value-ChangeIssue', $this->m_proposedValue, $this->m_sReason); + return Dict::Format('UI:CSVReport-Value-ChangeIssue', \utils::EscapeHtml($this->m_proposedValue)); + } + + public function GetDescription() + { + return $this->m_sReason; + } + /* + * @since 3.1.0 N°5305 + */ + public function GetDisplayableValueAndDescription(): string + { + return sprintf("%s. %s", + $this->GetDisplayableValue(), + $this->GetDescription() + ); } } class CellStatus_SearchIssue extends CellStatus_Issue { - public function __construct() + /** @var string|null $m_sAllowedValues */ + private $m_sAllowedValues; + + /** + * @since 3.1.0 N°5305 + * @var string $sSerializedSearch + */ + private $sSerializedSearch; + + /** @var string|null $m_sTargetClass */ + private $m_sTargetClass; + + /** + * CellStatus_SearchIssue constructor. + * @since 3.1.0 N°5305 + * + * @param string $sOql : main message + * @param string $sReason : main message + * @param null $sClass : used for additional message that provides allowed values for current class $sClass + * @param null $sAllowedValues : used for additional message that provides allowed values $sAllowedValues for current class + */ + public function __construct($sSerializedSearch, $sReason, $sClass=null, $sAllowedValues=null) { - parent::__construct(null, null, null); + parent::__construct(null, null, $sReason); + $this->sSerializedSearch = $sSerializedSearch; + $this->m_sAllowedValues = $sAllowedValues; + $this->m_sTargetClass = $sClass; + } + + public function GetDisplayableValue() + { + if (null === $this->m_sReason) { + return Dict::Format('UI:CSVReport-Value-NoMatch', ''); + } + + return $this->m_sReason; } public function GetDescription() { - return Dict::S('UI:CSVReport-Value-NoMatch'); + if (\utils::IsNullOrEmptyString($this->m_sAllowedValues) || + \utils::IsNullOrEmptyString($this->m_sTargetClass)) { + return ''; + } + + return Dict::Format('UI:CSVReport-Value-NoMatch-PossibleValues', $this->m_sTargetClass, $this->m_sAllowedValues); + } + + /** + * @since 3.1.0 N°5305 + * @return string + */ + public function GetSearchLinkUrl() + { + return sprintf("UI.php?operation=search&filter=%s", + rawurlencode($this->sSerializedSearch) + ); } } @@ -126,11 +201,24 @@ class CellStatus_NullIssue extends CellStatus_Issue class CellStatus_Ambiguous extends CellStatus_Issue { protected $m_iCount; + /** + * @since 3.1.0 N°5305 + * @var string + */ + protected $sSerializedSearch; - public function __construct($previousValue, $iCount, $sOql) + /** + * @since 3.1.0 N°5305 + * + * @param $previousValue + * @param int $iCount + * @param string $sSerializedSearch + * + */ + public function __construct($previousValue, $iCount, $sSerializedSearch) { $this->m_iCount = $iCount; - $this->m_sQuery = $sOql; + $this->sSerializedSearch = $sSerializedSearch; parent::__construct(null, $previousValue, ''); } @@ -139,12 +227,23 @@ class CellStatus_Ambiguous extends CellStatus_Issue $sCount = $this->m_iCount; return Dict::Format('UI:CSVReport-Value-Ambiguous', $sCount); } + + /** + * @since 3.1.0 N°5305 + * @return string + */ + public function GetSearchLinkUrl() + { + return sprintf("UI.php?operation=search&filter=%s", + rawurlencode($this->sSerializedSearch) + ); + } } /** * RowStatus - * A series of classes, keeping the information about a given row: could it be changed or not (and why)? + * A series of classes, keeping the information about a given row: could it be changed or not (and why)? * * @package iTopORM */ @@ -211,6 +310,26 @@ class RowStatus_Issue extends RowStatus } } +/** + * class dedicated to testability + * not used/ignored in csv imports UI/CLI + * @since 3.1.0 N°5305 + */ +class RowStatus_Error extends RowStatus +{ + /** @var string */ + protected $m_sError; + + public function __construct($sError) + { + $this->m_sError = $sError; + } + + public function GetDescription() + { + return $this->m_sError; + } +} /** * BulkChange @@ -220,17 +339,35 @@ class RowStatus_Issue extends RowStatus */ class BulkChange { - protected $m_sClass; + /** @var string */ + protected $m_sClass; protected $m_aData; // Note: hereafter, iCol maybe actually be any acceptable key (string) // #@# todo: rename the variables to sColIndex - protected $m_aAttList; // attcode => iCol - protected $m_aExtKeys; // aExtKeys[sExtKeyAttCode][sExtReconcKeyAttCode] = iCol; - protected $m_aReconcilKeys; // attcode (attcode = 'id' for the pkey) - protected $m_sSynchroScope; // OQL - if specified, then the missing items will be reported - protected $m_aOnDisappear; // array of attcode => value, values to be set when an object gets out of scope (ignored if no scope has been defined) - protected $m_sDateFormat; // Date format specification, see DateTime::createFromFormat - protected $m_bLocalizedValues; // Values in the data set are localized (see AttributeEnum) - protected $m_aExtKeysMappingCache; // Cache for resolving external keys based on the given search criterias + /** @var array attcode as key, iCol as value */ + protected $m_aAttList; + /** @var array> sExtKeyAttCode as key, array of sExtReconcKeyAttCode/iCol as value */ + protected $m_aExtKeys; + /** @var string[] list of attcode (attcode = 'id' for the pkey) */ + protected $m_aReconcilKeys; + /** @var string OQL - if specified, then the missing items will be reported */ + protected $m_sSynchroScope; + /** + * @var array attcode as key, attvalue as value. Values to be set when an object gets out of scope + * (ignored if no scope has been defined) + */ + protected $m_aOnDisappear; + /** + * @see DateTime::createFromFormat + * @var string Date format specification + */ + protected $m_sDateFormat; + /** + * @see AttributeEnum + * @var boolean true if Values in the data set are localized + */ + protected $m_bLocalizedValues; + /** @var array Cache for resolving external keys based on the given search criterias */ + protected $m_aExtKeysMappingCache; public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null, $sDateFormat = null, $bLocalize = false) { @@ -261,30 +398,30 @@ class BulkChange $this->m_sReportCsvSep = $sSeparator; $this->m_sReportCsvDelimiter = $sDelimiter; } - + protected function ResolveExternalKey($aRowData, $sAttCode, &$aResults) { $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); $oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass()); - foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol) + foreach ($this->m_aExtKeys[$sAttCode] as $sReconKeyAttCode => $iCol) { - if ($sForeignAttCode == 'id') + if ($sReconKeyAttCode == 'id') { $value = (int) $aRowData[$iCol]; } else { // The foreign attribute is one of our reconciliation key - $oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode); + $oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sReconKeyAttCode); $value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); } - $oReconFilter->AddCondition($sForeignAttCode, $value, '='); + $oReconFilter->AddCondition($sReconKeyAttCode, $value, '='); $aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol])); } $oExtObjects = new CMDBObjectSet($oReconFilter); $aKeys = $oExtObjects->ToArray(); - return array($oReconFilter->ToOql(), $aKeys); + return array($oReconFilter, $aKeys); } // Returns true if the CSV data specifies that the external key must be left undefined @@ -318,10 +455,10 @@ class BulkChange { $aResults = array(); $aErrors = array(); - + // External keys reconciliation // - foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig) + foreach($this->m_aExtKeys as $sAttCode => $aReconKeys) { // Skip external keys used for the reconciliation process // if (!array_key_exists($sAttCode, $this->m_aAttList)) continue; @@ -330,7 +467,7 @@ class BulkChange if ($this->IsNullExternalKeySpec($aRowData, $sAttCode)) { - foreach ($aKeyConfig as $sForeignAttCode => $iCol) + foreach ($aReconKeys as $sReconKeyAttCode => $iCol) { // Default reporting // $aRowData[$iCol] is always null @@ -352,25 +489,24 @@ class BulkChange $oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass()); $aCacheKeys = array(); - foreach ($aKeyConfig as $sForeignAttCode => $iCol) + foreach ($aReconKeys as $sReconKeyAttCode => $iCol) { // The foreign attribute is one of our reconciliation key - if ($sForeignAttCode == 'id') + if ($sReconKeyAttCode == 'id') { $value = $aRowData[$iCol]; } else { - $oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode); + $oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sReconKeyAttCode); $value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); } $aCacheKeys[] = $value; - $oReconFilter->AddCondition($sForeignAttCode, $value, '='); + $oReconFilter->AddCondition($sReconKeyAttCode, $value, '='); $aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol])); } $sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query... $iForeignKey = null; - $sOQL = ''; // TODO: check if *too long* keys can lead to collisions... and skip the cache in such a case... if (!array_key_exists($sAttCode, $this->m_aExtKeysMappingCache)) { @@ -379,9 +515,8 @@ class BulkChange if (array_key_exists($sCacheKey, $this->m_aExtKeysMappingCache[$sAttCode])) { // Cache hit - $iCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c']; + $iObjectFoundCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c']; $iForeignKey = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['k']; - $sOQL = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['oql']; // Record the hit $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['h']++; } @@ -389,34 +524,35 @@ class BulkChange { // Cache miss, let's initialize it $oExtObjects = new CMDBObjectSet($oReconFilter); - $iCount = $oExtObjects->Count(); - if ($iCount == 1) + $iObjectFoundCount = $oExtObjects->Count(); + if ($iObjectFoundCount == 1) { $oForeignObj = $oExtObjects->Fetch(); $iForeignKey = $oForeignObj->GetKey(); } $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array( - 'c' => $iCount, + 'c' => $iObjectFoundCount, 'k' => $iForeignKey, 'oql' => $oReconFilter->ToOql(), 'h' => 0, // number of hits on this cache entry ); } - switch($iCount) + switch($iObjectFoundCount) { case 0: - $aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound'); - $aResults[$sAttCode]= new CellStatus_SearchIssue(); - break; - + $oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter); + $aResults[$sAttCode] = $oCellStatus_SearchIssue; + $aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound'); + break; + case 1: - // Do change the external key attribute - $oTargetObj->Set($sAttCode, $iForeignKey); - break; - + // Do change the external key attribute + $oTargetObj->Set($sAttCode, $iForeignKey); + break; + default: - $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iCount); - $aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iCount, $sOQL); + $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iObjectFoundCount); + $aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iObjectFoundCount, $oReconFilter->serialize()); } } @@ -433,7 +569,7 @@ class BulkChange else { $aResults[$sAttCode]= new CellStatus_Modify($iForeignObj, $oTargetObj->GetOriginal($sAttCode)); - foreach ($aKeyConfig as $sForeignAttCode => $iCol) + foreach ($aReconKeys as $sReconKeyAttCode => $iCol) { // Report the change on reconciliation values as well $aResults[$iCol] = new CellStatus_Modify(utils::HtmlEntities($aRowData[$iCol])); @@ -446,7 +582,7 @@ class BulkChange } } } - + // Set the object attributes // foreach ($this->m_aAttList as $sAttCode => $iCol) @@ -487,7 +623,13 @@ class BulkChange $value = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); if (is_null($value) && (strlen($aRowData[$iCol]) > 0)) { - $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode); + if ($oAttDef instanceof AttributeEnum || $oAttDef instanceof AttributeTagSet){ + /** @var AttributeDefinition $oAttributeDefinition */ + $oAttributeDefinition = $oAttDef; + $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-AllowedValues', $sAttCode, implode(',', $oAttributeDefinition->GetAllowedValues())); + } else { + $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode); + } } else { @@ -504,7 +646,7 @@ class BulkChange } } } - + // Reporting on fields // $aChangedFields = $oTargetObj->ListChanges(); @@ -556,7 +698,7 @@ class BulkChange } } } - + // Checks // $res = $oTargetObj->CheckConsistency(); @@ -567,12 +709,101 @@ class BulkChange } return $aResults; } - + + /** + * search with current permissions did not match + * let's search why and give some more feedbacks to the user through proper labels + * + * @param DBObjectSearch $oDbSearchWithConditions search used to find external key + * + * @return \CellStatus_SearchIssue + * @throws \CoreException + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + * + * @since 3.1.0 N°5305 + */ + protected function GetCellSearchIssue($oDbSearchWithConditions) : CellStatus_SearchIssue { + //current search with current permissions did not match + //let's search why and give some more feedback to the user + + $sSerializedSearch = $oDbSearchWithConditions->serialize(); + + // Count all objects with all permissions without any condition + $oDbSearchWithoutAnyCondition = new DBObjectSearch($oDbSearchWithConditions->GetClass()); + $oDbSearchWithoutAnyCondition->AllowAllData(true); + $oExtObjectSet = new CMDBObjectSet($oDbSearchWithoutAnyCondition); + $iAllowAllDataObjectCount = $oExtObjectSet->Count(); + + if ($iAllowAllDataObjectCount === 0) { + $sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject', $oDbSearchWithConditions->GetClass()); + return new CellStatus_SearchIssue($sSerializedSearch, $sReason); + } + + // Count all objects with current user permissions + $oDbSearchWithoutAnyCondition->AllowAllData(false); + $oExtObjectSetWithCurrentUserPermissions = new CMDBObjectSet($oDbSearchWithoutAnyCondition); + $iCurrentUserRightsObjectCount = $oExtObjectSetWithCurrentUserPermissions->Count(); + + if ($iCurrentUserRightsObjectCount === 0){ + // No objects visible by current user + $sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser', $oDbSearchWithConditions->GetClass()); + return new CellStatus_SearchIssue($sSerializedSearch, $sReason); + } + + try{ + $aDisplayedAllowedValues = []; + // Possibles values are displayed to UI user. we have to limit the amount of displayed values + $oExtObjectSetWithCurrentUserPermissions->SetLimit(4); + for($i = 0; $i < 3; $i++){ + /** @var \DBObject $oVisibleObject */ + $oVisibleObject = $oExtObjectSetWithCurrentUserPermissions->Fetch(); + if (is_null($oVisibleObject)){ + break; + } + + $aCurrentAllowedValueFields = []; + foreach ($oDbSearchWithConditions->GetInternalParams() as $sForeignAttCode => $sValue){ + $aCurrentAllowedValueFields[] = $oVisibleObject->Get($sForeignAttCode); + } + $aDisplayedAllowedValues[] = implode(" ", $aCurrentAllowedValueFields); + + } + $allowedValues = implode(", ", $aDisplayedAllowedValues); + if ($oExtObjectSetWithCurrentUserPermissions->Count() > 3){ + $allowedValues .= "..."; + } + } catch(Exception $e) { + IssueLog::Error("failure during CSV import when fetching few visible objects: ", null, + [ 'target_class' => $oDbSearchWithConditions->GetClass(), 'criteria' => $oDbSearchWithConditions->GetCriteria(), 'message' => $e->getMessage()] + ); + $sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser', $oDbSearchWithConditions->GetClass()); + return new CellStatus_SearchIssue($sSerializedSearch, $sReason); + } + + if ($iAllowAllDataObjectCount != $iCurrentUserRightsObjectCount) { + // No match and some objects NOT visible by current user. including current search maybe... + $sReason = Dict::Format('UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser', $oDbSearchWithConditions->GetClass()); + return new CellStatus_SearchIssue($sSerializedSearch, $sReason, $oDbSearchWithConditions->GetClass(), $allowedValues); + } + + // No match. This is not linked to any right issue + // Possible values: DD,DD + $aCurrentValueFields = []; + foreach ($oDbSearchWithConditions->GetInternalParams() as $sValue){ + $aCurrentValueFields[] = $sValue; + } + $value =implode(" ", $aCurrentValueFields); + $sReason = Dict::Format('UI:CSVReport-Value-NoMatch', $value); + return new CellStatus_SearchIssue($sSerializedSearch, $sReason, $oDbSearchWithConditions->GetClass(), $allowedValues); + } + protected function PrepareMissingObject(&$oTargetObj, &$aErrors) { $aResults = array(); $aErrors = array(); - + // External keys // foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig) @@ -585,7 +816,7 @@ class BulkChange $aResults[$iCol] = new CellStatus_Void('?'); } } - + // Update attributes // foreach($this->m_aOnDisappear as $sAttCode => $value) @@ -596,7 +827,7 @@ class BulkChange } $oTargetObj->Set($sAttCode, $value); } - + // Reporting on fields // $aChangedFields = $oTargetObj->ListChanges(); @@ -616,7 +847,7 @@ class BulkChange $aResults[$iCol]= new CellStatus_Void($oTargetObj->Get($sAttCode)); } } - + // Checks // $res = $oTargetObj->CheckConsistency(); @@ -674,14 +905,16 @@ class BulkChange } $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors); - + if (count($aErrors) > 0) { $sErrors = implode(', ', $aErrors); $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute')); + //__ERRORS__ used by tests only + $aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors); return $oTargetObj; } - + // Check that any external key will have a value proposed $aMissingKeys = array(); foreach (MetaModel::GetExternalKeys($this->m_sClass) as $sExtKeyAttCode => $oExtKey) @@ -689,7 +922,7 @@ class BulkChange if (!$oExtKey->IsNullAllowed()) { if (!array_key_exists($sExtKeyAttCode, $this->m_aExtKeys) && !array_key_exists($sExtKeyAttCode, $this->m_aAttList)) - { + { $aMissingKeys[] = $oExtKey->GetLabel(); } } @@ -745,14 +978,16 @@ class BulkChange { $sErrors = implode(', ', $aErrors); $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute')); + //__ERRORS__ used by tests only + $aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors); return; } - + $aChangedFields = $oTargetObj->ListChanges(); if (count($aChangedFields) > 0) { $aResult[$iRow]["__STATUS__"] = new RowStatus_Modify(count($aChangedFields)); - + // Optionaly record the results // if ($oChange) @@ -794,9 +1029,11 @@ class BulkChange { $sErrors = implode(', ', $aErrors); $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute')); + //__ERRORS__ used by tests only + $aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors); return; } - + $aChangedFields = $oTargetObj->ListChanges(); if (count($aChangedFields) > 0) { @@ -821,7 +1058,7 @@ class BulkChange $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(0); } } - + public function Process(CMDBChange $oChange = null) { if ($oChange) @@ -866,7 +1103,7 @@ class BulkChange foreach ($this->m_aAttList as $sAttCode => $iCol) { if ($sAttCode == 'id') continue; - + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); if ($oAttDef instanceof AttributeDateTime) // AttributeDate is derived from AttributeDateTime { @@ -881,14 +1118,18 @@ class BulkChange $sFormat = $sDateFormat; } $oFormat = new DateTimeFormat($sFormat); + $sDateExample = $oFormat->Format(new DateTime('2022-10-23 16:25:33')); $sRegExp = $oFormat->ToRegExpr('/'); - if (!preg_match($sRegExp, $this->m_aData[$iRow][$iCol])) + $sErrorMsg = Dict::Format('UI:CSVReport-Row-Issue-ExpectedDateFormat', $sDateExample); + if (!preg_match($sRegExp, $sValue)) { $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat')); + $aResult[$iRow][$iCol] = new CellStatus_Issue(utils::HtmlEntities($sValue), null, $sErrorMsg); + } else { - $oDate = DateTime::createFromFormat($sFormat, $this->m_aData[$iRow][$iCol]); + $oDate = DateTime::createFromFormat($sFormat, $sValue); if ($oDate !== false) { $sNewDate = $oDate->format($oAttDef->GetInternalFormat()); @@ -898,7 +1139,7 @@ class BulkChange { // Leave the cell unchanged $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat')); - $aResult[$iRow][$sAttCode] = new CellStatus_Issue(null, utils::HtmlEntities($this->m_aData[$iRow][$iCol]), Dict::S('UI:CSVReport-Row-Issue-DateFormat')); + $aResult[$iRow][$iCol] = new CellStatus_Issue($sValue, null, $sErrorMsg); } } } @@ -952,23 +1193,26 @@ class BulkChange else { // The value has to be found or verified - list($sQuery, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]); - + + /** var DBObjectSearch $oReconFilter */ + list($oReconFilter, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]); + if (count($aMatches) == 1) { $oRemoteObj = reset($aMatches); // first item $valuecondition = $oRemoteObj->GetKey(); $aResult[$iRow][$sAttCode] = new CellStatus_Void($oRemoteObj->GetKey()); - } + } elseif (count($aMatches) == 0) { - $aResult[$iRow][$sAttCode] = new CellStatus_SearchIssue(); - } + $oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter); + $aResult[$iRow][$sAttCode] = $oCellStatus_SearchIssue; + } else { - $aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $sQuery); + $aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $oReconFilter->serialize()); } - } + } } else { @@ -1019,7 +1263,7 @@ class BulkChange default: // Found several matches, ambiguous $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Ambiguous')); - $aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->ToOql()); + $aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->serialize()); $aResult[$iRow]["finalclass"]= 'n/a'; } } @@ -1110,7 +1354,7 @@ class BulkChange } } $oBulkChanges->Seek(0); - + $aDetails = array(); while ($oChange = $oBulkChanges->Fetch()) { @@ -1274,7 +1518,7 @@ EOF $oOldTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('oldvalue')); $sOldValue = $oOldTarget->GetHyperlink(); } - + $sNewValue = Dict::S('UI:UndefinedObject'); if ($oOperation->Get('newvalue') != 0) { @@ -1300,11 +1544,11 @@ EOF } else { - $aAttributes[$sAttCode] = 1; + $aAttributes[$sAttCode] = 1; } } } - + $aDetails = array(); foreach($aObjects as $iUId => $aObjData) { @@ -1356,6 +1600,6 @@ EOF $aConfig[$sAttCode] = array('label' => MetaModel::GetLabel($sClass, $sAttCode), 'description' => MetaModel::GetDescription($sClass, $sAttCode)); } $oPage->table($aConfig, $aDetails); - } + } } diff --git a/css/backoffice/pages/_csv-import.scss b/css/backoffice/pages/_csv-import.scss index 55357e3f4..b03897a53 100644 --- a/css/backoffice/pages/_csv-import.scss +++ b/css/backoffice/pages/_csv-import.scss @@ -27,11 +27,6 @@ tr.ibo-csv-import--row-unchanged td { border-bottom: 1px $ibo-color-grey-400 solid; } -.wizContainer table tr.ibo-csv-import--row-error td { - border-bottom: 1px $ibo-color-grey-400 solid; - background-color: $ibo-color-red-200; -} - tr.ibo-csv-import--row-modified td { border-bottom: 1px $ibo-color-grey-400 solid; } @@ -44,4 +39,4 @@ tr.ibo-csv-import--row-added td { font-size: 4em; color: $ibo-color-primary-400; margin: 20px; -} \ No newline at end of file +} diff --git a/dictionaries/cs.dictionary.itop.ui.php b/dictionaries/cs.dictionary.itop.ui.php index ddf9906f0..66113e5a3 100755 --- a/dictionaries/cs.dictionary.itop.ui.php +++ b/dictionaries/cs.dictionary.itop.ui.php @@ -644,9 +644,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'UI:UniversalSearch:LabelSelectTheClass' => 'Vyberte třídu pro hledání: ', 'UI:CSVReport-Value-Modified' => 'Upraveno', - 'UI:CSVReport-Value-SetIssue' => 'Nemůže být změněno - důvod: %1$s', - 'UI:CSVReport-Value-ChangeIssue' => 'Nemůže být změněno na %1$s - důvod: %2$s', - 'UI:CSVReport-Value-NoMatch' => 'Žádná shoda', + 'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~', + 'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~', + 'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~', 'UI:CSVReport-Value-Missing' => 'Chybí povinná hodnota', 'UI:CSVReport-Value-Ambiguous' => 'Nejednoznačné: nalezeno %1$s objektů', 'UI:CSVReport-Row-Unchanged' => 'nezměněn', diff --git a/dictionaries/da.dictionary.itop.ui.php b/dictionaries/da.dictionary.itop.ui.php index 19b632898..ac5b235bf 100644 --- a/dictionaries/da.dictionary.itop.ui.php +++ b/dictionaries/da.dictionary.itop.ui.php @@ -633,9 +633,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'UI:UniversalSearch:LabelSelectTheClass' => 'Vælg klasse at søge efter: ', 'UI:CSVReport-Value-Modified' => 'Ændret', - 'UI:CSVReport-Value-SetIssue' => 'Kunne ikke ændres - årsag: %1$s', - 'UI:CSVReport-Value-ChangeIssue' => 'Kunne ikke ændres til %1$s - årsag: %2$s', - 'UI:CSVReport-Value-NoMatch' => 'No match', + 'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~', + 'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~', + 'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~', 'UI:CSVReport-Value-Missing' => 'Mangler obligatorisk værdi', 'UI:CSVReport-Value-Ambiguous' => 'Tvetydig: fandt %1$s objekter', 'UI:CSVReport-Row-Unchanged' => 'Uændret', diff --git a/dictionaries/de.dictionary.itop.ui.php b/dictionaries/de.dictionary.itop.ui.php index f6249a11f..ff71879d4 100644 --- a/dictionaries/de.dictionary.itop.ui.php +++ b/dictionaries/de.dictionary.itop.ui.php @@ -633,9 +633,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'UI:UniversalSearch:LabelSelectTheClass' => 'Wählen Sie für die Suche die Klasse aus: ', 'UI:CSVReport-Value-Modified' => 'Modifiziert', - 'UI:CSVReport-Value-SetIssue' => 'Konnte nicht geändert werden - Grund: %1$s', - 'UI:CSVReport-Value-ChangeIssue' => 'Konnte nicht zu %1$s geändert werden - Grund: %2$s', - 'UI:CSVReport-Value-NoMatch' => 'Kein Treffer', + 'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~', + 'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~', + 'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~', 'UI:CSVReport-Value-Missing' => 'Pflichtfeld fehlt', 'UI:CSVReport-Value-Ambiguous' => 'Doppeldeutig: %1$s Objekte gefunden', 'UI:CSVReport-Row-Unchanged' => 'Unverändert', diff --git a/dictionaries/en.dictionary.itop.ui.php b/dictionaries/en.dictionary.itop.ui.php index 58194f286..84a4dba55 100644 --- a/dictionaries/en.dictionary.itop.ui.php +++ b/dictionaries/en.dictionary.itop.ui.php @@ -656,9 +656,14 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'UI:UniversalSearch:LabelSelectTheClass' => 'Select the class to search: ', 'UI:CSVReport-Value-Modified' => 'Modified', - 'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s', - 'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s', - 'UI:CSVReport-Value-NoMatch' => 'No match', + 'UI:CSVReport-Value-SetIssue' => 'Invalid value for attribute', + 'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value', + 'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'', + 'UI:CSVReport-Value-NoMatch-PossibleValues' => 'Some possible \'%1$s\' value(s): %2$s', + 'UI:CSVReport-Value-NoMatch-NoObject' => 'There are no \'%1$s\' objects', + 'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => 'There are no \'%1$s\' objects found with your current profile', + 'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => 'There are some \'%1$s\' objects not visible with your current profile', + 'UI:CSVReport-Value-Missing' => 'Missing mandatory value', 'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects', 'UI:CSVReport-Row-Unchanged' => 'unchanged', @@ -672,11 +677,13 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'UI:CSVReport-Value-Issue-Readonly' => 'The attribute \'%1$s\' is read-only and cannot be modified (current value: %2$s, proposed value: %3$s)', 'UI:CSVReport-Value-Issue-Format' => 'Failed to process input: %1$s', 'UI:CSVReport-Value-Issue-NoMatch' => 'Unexpected value for attribute \'%1$s\': no match found, check spelling', + 'UI:CSVReport-Value-Issue-AllowedValues' => 'Allowed \'%1$s\' value(s): %2$s', 'UI:CSVReport-Value-Issue-Unknown' => 'Unexpected value for attribute \'%1$s\': %2$s', 'UI:CSVReport-Row-Issue-Inconsistent' => 'Attributes not consistent with each others: %1$s', 'UI:CSVReport-Row-Issue-Attribute' => 'Unexpected attribute value(s)', 'UI:CSVReport-Row-Issue-MissingExtKey' => 'Could not be created, due to missing external key(s): %1$s', 'UI:CSVReport-Row-Issue-DateFormat' => 'wrong date format', + 'UI:CSVReport-Row-Issue-ExpectedDateFormat' => 'Expected format: %1$s', 'UI:CSVReport-Row-Issue-Reconciliation' => 'failed to reconcile', 'UI:CSVReport-Row-Issue-Ambiguous' => 'ambiguous reconciliation', 'UI:CSVReport-Row-Issue-Internal' => 'Internal error: %1$s, %2$s', diff --git a/dictionaries/es_cr.dictionary.itop.ui.php b/dictionaries/es_cr.dictionary.itop.ui.php index 95c9d399e..aab5c46ec 100644 --- a/dictionaries/es_cr.dictionary.itop.ui.php +++ b/dictionaries/es_cr.dictionary.itop.ui.php @@ -644,9 +644,9 @@ Esperamos distrute de esta versión tanto como nosotros la imaginamos y creamos. 'UI:UniversalSearch:LabelSelectTheClass' => 'Seleccione la clase a buscar: ', 'UI:CSVReport-Value-Modified' => 'Modificado', - 'UI:CSVReport-Value-SetIssue' => 'No puede ser modificado - motivo: %1$s', - 'UI:CSVReport-Value-ChangeIssue' => 'No puede ser cambiado a %1$s - motivo: %2$s', - 'UI:CSVReport-Value-NoMatch' => 'No hay Coincidencias', + 'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~', + 'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~', + 'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~', 'UI:CSVReport-Value-Missing' => 'Falta valor obligatorio', 'UI:CSVReport-Value-Ambiguous' => 'Ambigüedad: encontrados %1$s objetos', 'UI:CSVReport-Row-Unchanged' => 'Sin Cambios', diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php index e36507466..d23a2fc58 100644 --- a/dictionaries/fr.dictionary.itop.ui.php +++ b/dictionaries/fr.dictionary.itop.ui.php @@ -639,9 +639,14 @@ Nous espérons que vous aimerez cette version autant que nous avons eu du plaisi 'UI:UniversalSearch:LabelSelectTheClass' => 'Sélectionnez le type d\'objets à rechercher : ', 'UI:CSVReport-Value-Modified' => 'Modifié', - 'UI:CSVReport-Value-SetIssue' => 'Modification impossible - cause : %1$s', - 'UI:CSVReport-Value-ChangeIssue' => 'Ne peut pas prendre la valeur \'%1$s\' - cause : %2$s', - 'UI:CSVReport-Value-NoMatch' => 'Pas de correspondance', + 'UI:CSVReport-Value-SetIssue' => 'Valeur invalide', + 'UI:CSVReport-Value-ChangeIssue' => 'Ne peut pas prendre la valeur \'%1$s\'', + 'UI:CSVReport-Value-NoMatch' => 'Pas de correspondance avec \'%1$s\'', + 'UI:CSVReport-Value-NoMatch-PossibleValues' => 'Valeur(s) possible(s) pour l\'objet \'%1$s\' : %2$s', + 'UI:CSVReport-Value-NoMatch-NoObject' => 'Il n\'y a aucun objet \'%1$s\'', + 'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => 'Il n\'y a aucun objet \'%1$s\' visible par votre utilisateur', + 'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => 'Il existe des objet(s) \'%1$s\' non visible(s) par votre utilisateur', + 'UI:CSVReport-Value-Missing' => 'Absence de valeur obligatoire', 'UI:CSVReport-Value-Ambiguous' => 'Ambigüité: %1$d objets trouvés', 'UI:CSVReport-Row-Unchanged' => 'inchangé', diff --git a/dictionaries/hu.dictionary.itop.ui.php b/dictionaries/hu.dictionary.itop.ui.php index 89929addd..71661a6cf 100755 --- a/dictionaries/hu.dictionary.itop.ui.php +++ b/dictionaries/hu.dictionary.itop.ui.php @@ -633,9 +633,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'UI:UniversalSearch:LabelSelectTheClass' => 'Keresendő osztály kiválasztása:', 'UI:CSVReport-Value-Modified' => 'Modified~~', - 'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s~~', - 'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s~~', - 'UI:CSVReport-Value-NoMatch' => 'No match~~', + 'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~', + 'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~', + 'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~', 'UI:CSVReport-Value-Missing' => 'Missing mandatory value~~', 'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects~~', 'UI:CSVReport-Row-Unchanged' => 'unchanged~~', diff --git a/dictionaries/it.dictionary.itop.ui.php b/dictionaries/it.dictionary.itop.ui.php index 22c6d29fc..ecbf39612 100644 --- a/dictionaries/it.dictionary.itop.ui.php +++ b/dictionaries/it.dictionary.itop.ui.php @@ -644,9 +644,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'UI:UniversalSearch:LabelSelectTheClass' => 'Seleziona la classe per la ricerca: ', 'UI:CSVReport-Value-Modified' => 'Modified~~', - 'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s~~', - 'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s~~', - 'UI:CSVReport-Value-NoMatch' => 'No match~~', + 'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~', + 'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~', + 'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~', 'UI:CSVReport-Value-Missing' => 'Missing mandatory value~~', 'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects~~', 'UI:CSVReport-Row-Unchanged' => 'unchanged~~', diff --git a/dictionaries/ja.dictionary.itop.ui.php b/dictionaries/ja.dictionary.itop.ui.php index 3c78e7d90..e3e4376fc 100644 --- a/dictionaries/ja.dictionary.itop.ui.php +++ b/dictionaries/ja.dictionary.itop.ui.php @@ -633,9 +633,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'UI:UniversalSearch:LabelSelectTheClass' => '検索するクラスを選択してください。', 'UI:CSVReport-Value-Modified' => '修正済み', - 'UI:CSVReport-Value-SetIssue' => '変更出来ません - 理由: %1$s', - 'UI:CSVReport-Value-ChangeIssue' => '%1$s へ変更出来ません - 理由: %2$s', - 'UI:CSVReport-Value-NoMatch' => 'マッチしません', + 'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~', + 'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~', + 'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~', 'UI:CSVReport-Value-Missing' => '必須の値がありません', 'UI:CSVReport-Value-Ambiguous' => 'あいまいな値: %1$s オブジェクト', 'UI:CSVReport-Row-Unchanged' => '未変更', diff --git a/dictionaries/nl.dictionary.itop.ui.php b/dictionaries/nl.dictionary.itop.ui.php index 9381d81c1..f857fa56a 100644 --- a/dictionaries/nl.dictionary.itop.ui.php +++ b/dictionaries/nl.dictionary.itop.ui.php @@ -644,9 +644,9 @@ We hopen dat je even hard van deze versie geniet als dat we zelf ervan hebben ge 'UI:UniversalSearch:LabelSelectTheClass' => 'Selecteer de klasse om te zoeken: ', 'UI:CSVReport-Value-Modified' => 'Aangepast', - 'UI:CSVReport-Value-SetIssue' => 'Kon niet worden aangepast - reden: %1$s', - 'UI:CSVReport-Value-ChangeIssue' => 'Kon niet worden aangepast naar %1$s - reden: %2$s', - 'UI:CSVReport-Value-NoMatch' => 'Geen match', + 'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~', + 'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~', + 'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~', 'UI:CSVReport-Value-Missing' => 'Ontbrekende verplichte waarde', 'UI:CSVReport-Value-Ambiguous' => 'Onduidelijk: gevonden %1$s objecten', 'UI:CSVReport-Row-Unchanged' => 'onveranderd', diff --git a/dictionaries/pl.dictionary.itop.ui.php b/dictionaries/pl.dictionary.itop.ui.php index 20594a471..ac9eeae14 100644 --- a/dictionaries/pl.dictionary.itop.ui.php +++ b/dictionaries/pl.dictionary.itop.ui.php @@ -643,9 +643,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'UI:UniversalSearch:LabelSelectTheClass' => 'Wybierz klasę do przeszukania: ', 'UI:CSVReport-Value-Modified' => 'Zmodyfikowano', - 'UI:CSVReport-Value-SetIssue' => 'Nie można było zmienić - powód: %1$s', - 'UI:CSVReport-Value-ChangeIssue' => 'Nie można zmienić na %1$s - powód: %2$s', - 'UI:CSVReport-Value-NoMatch' => 'Nie pasuje', + 'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~', + 'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~', + 'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~', 'UI:CSVReport-Value-Missing' => 'Brak wymaganej wartości', 'UI:CSVReport-Value-Ambiguous' => 'Uwaga: znaleziono %1$s obiektów', 'UI:CSVReport-Row-Unchanged' => 'niezmieniony', diff --git a/dictionaries/pt_br.dictionary.itop.ui.php b/dictionaries/pt_br.dictionary.itop.ui.php index 58099b866..400151425 100644 --- a/dictionaries/pt_br.dictionary.itop.ui.php +++ b/dictionaries/pt_br.dictionary.itop.ui.php @@ -644,9 +644,9 @@ Esperamos que você goste desta versão tanto quanto gostamos de imaginá-la e c 'UI:UniversalSearch:LabelSelectTheClass' => 'Selecione a classe para pesquisar: ', 'UI:CSVReport-Value-Modified' => 'Modificado', - 'UI:CSVReport-Value-SetIssue' => 'Não pode ser modificado - motivo: %1$s', - 'UI:CSVReport-Value-ChangeIssue' => 'Não pode ser modificado para %1$s - motivo: %2$s', - 'UI:CSVReport-Value-NoMatch' => 'Não corresponde', + 'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~', + 'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~', + 'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~', 'UI:CSVReport-Value-Missing' => 'Faltando valor obrigatório', 'UI:CSVReport-Value-Ambiguous' => 'Ambíguo: encontrado %1$s objeto(s)', 'UI:CSVReport-Row-Unchanged' => 'inalterado', diff --git a/dictionaries/ru.dictionary.itop.ui.php b/dictionaries/ru.dictionary.itop.ui.php index edc0a00ae..9c9473d4e 100644 --- a/dictionaries/ru.dictionary.itop.ui.php +++ b/dictionaries/ru.dictionary.itop.ui.php @@ -645,9 +645,9 @@ Dict::Add('RU RU', 'Russian', 'Русский', array( 'UI:UniversalSearch:LabelSelectTheClass' => 'Выбор класса для поиска: ', 'UI:CSVReport-Value-Modified' => 'Изменен', - 'UI:CSVReport-Value-SetIssue' => 'Не может быть изменен - причина: %1$s', - 'UI:CSVReport-Value-ChangeIssue' => 'Не может быть изменен %1$s - причина: %2$s', - 'UI:CSVReport-Value-NoMatch' => 'Нет совпадений', + 'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~', + 'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~', + 'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~', 'UI:CSVReport-Value-Missing' => 'Отсутствует обязательное значение', 'UI:CSVReport-Value-Ambiguous' => 'Неоднозначное сопоставление: найдено %1$s объектов', 'UI:CSVReport-Row-Unchanged' => 'без изменений', diff --git a/dictionaries/sk.dictionary.itop.ui.php b/dictionaries/sk.dictionary.itop.ui.php index d1a1960ca..18be33363 100644 --- a/dictionaries/sk.dictionary.itop.ui.php +++ b/dictionaries/sk.dictionary.itop.ui.php @@ -634,9 +634,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'UI:UniversalSearch:LabelSelectTheClass' => 'Vyberte triedu na vyhľadávanie: ', 'UI:CSVReport-Value-Modified' => 'Upravený', - 'UI:CSVReport-Value-SetIssue' => 'Nemožno zmeniť - dôvod: %1$s', - 'UI:CSVReport-Value-ChangeIssue' => 'Nemožno zmeniť na %1$s - dôvod: %2$s', - 'UI:CSVReport-Value-NoMatch' => 'Žiadna zhoda', + 'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~', + 'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~', + 'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~', 'UI:CSVReport-Value-Missing' => 'Chýbajúca povinná hodnota', 'UI:CSVReport-Value-Ambiguous' => 'Nejednoznačné: nájdených %1$s objektov', 'UI:CSVReport-Row-Unchanged' => 'Nezmený', diff --git a/dictionaries/tr.dictionary.itop.ui.php b/dictionaries/tr.dictionary.itop.ui.php index 7f1b7264c..76985ee9c 100644 --- a/dictionaries/tr.dictionary.itop.ui.php +++ b/dictionaries/tr.dictionary.itop.ui.php @@ -661,9 +661,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'UI:UniversalSearch:LabelSelectTheClass' => 'Aranacak sınıfı seçiniz: ', 'UI:CSVReport-Value-Modified' => 'Değiştiridi', - 'UI:CSVReport-Value-SetIssue' => 'Değiştirilemedi - Sebep: %1$s', - 'UI:CSVReport-Value-ChangeIssue' => '%1$s olarak değiştirilemedi - Sebep: %2$s', - 'UI:CSVReport-Value-NoMatch' => 'Eşleşme yok', + 'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~', + 'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~', + 'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~', 'UI:CSVReport-Value-Missing' => 'Eksik Zorunlu Değer', 'UI:CSVReport-Value-Ambiguous' => 'Belirsiz: %1$s nesnelerini buldum', 'UI:CSVReport-Row-Unchanged' => 'Değiştirilmedi', diff --git a/dictionaries/zh_cn.dictionary.itop.ui.php b/dictionaries/zh_cn.dictionary.itop.ui.php index 8b05300d7..0a48df076 100644 --- a/dictionaries/zh_cn.dictionary.itop.ui.php +++ b/dictionaries/zh_cn.dictionary.itop.ui.php @@ -649,9 +649,9 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array( 'UI:UniversalSearch:LabelSelectTheClass' => '选择要搜索的类别: ', 'UI:CSVReport-Value-Modified' => '已修改', - 'UI:CSVReport-Value-SetIssue' => '无法修改 - 原因: %1$s', - 'UI:CSVReport-Value-ChangeIssue' => '无法修改成 %1$s - 原因: %2$s', - 'UI:CSVReport-Value-NoMatch' => '不匹配', + 'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~', + 'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~', + 'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~', 'UI:CSVReport-Value-Missing' => '缺少必填项', 'UI:CSVReport-Value-Ambiguous' => '模糊匹配: 找到 %1$s 个对象', 'UI:CSVReport-Row-Unchanged' => '保持不变', diff --git a/pages/csvimport.php b/pages/csvimport.php index 31b3e968d..15e42ea7f 100644 --- a/pages/csvimport.php +++ b/pages/csvimport.php @@ -138,7 +138,7 @@ try { } return $aResult; } - + /** * Return the most frequent (and regularly occuring) character among the given set, in the specified lines * @param array $aCSVData The input data, one entry per line @@ -174,7 +174,7 @@ try { } $iLine++; } - + $aScores = array(); foreach($aGuesses as $sSep => $aData) { @@ -185,7 +185,7 @@ try { $sSeparator = $aKeys[0]; // Take the first key, the one with the best score return $sSeparator; } - + /** * Try to predict the CSV parameters based on the input data * @param string $sCSVData The input data @@ -196,10 +196,10 @@ try { $aData = explode("\n", $sCSVData); $sSeparator = GuessFromFrequency($aData, array("\t", ',', ';', '|')); // Guess the most frequent (and regular) character on each line $sQualifier = GuessFromFrequency($aData, array('"', "'")); // Guess the most frequent (and regular) character on each line - + return array('separator' => $sSeparator, 'qualifier' => $sQualifier); } - + /** * Display a banner for the special "synchro" mode * @param WebPage $oP The Page for the output @@ -215,6 +215,7 @@ try { * Add a paragraph to the body of the page * * @param string $s_html + * @param ?string $sLinkUrl * * @return string */ @@ -259,9 +260,9 @@ try { $sSynchroScope = utils::ReadParam('synchro_scope', '', false, 'raw_data'); $sDateTimeFormat = utils::ReadParam('date_time_format', 'default'); $sCustomDateTimeFormat = utils::ReadParam('custom_date_time_format', (string)AttributeDateTime::GetFormat(), false, 'raw_data'); - + $sChosenDateFormat = ($sDateTimeFormat == 'default') ? (string)AttributeDateTime::GetFormat() : $sCustomDateTimeFormat; - + if (!empty($sSynchroScope)) { $oSearch = DBObjectSearch::FromOQL($sSynchroScope); @@ -276,7 +277,7 @@ try { $sSynchroScope = ''; $aSynchroUpdate = null; } - + // Parse the data set $oCSVParser = new CSVParser($sCSVData, $sSeparator, $sTextQualifier, MetaModel::GetConfig()->Get('max_execution_time_per_loop')); $aData = $oCSVParser->ToArray($iSkippedLines); @@ -286,10 +287,10 @@ try { $aResult[] = $sTextQualifier.implode($sTextQualifier.$sSeparator.$sTextQualifier, array_shift($aData)).$sTextQualifier; // Remove the first line and store it in case of error $iRealSkippedLines++; } - + // Format for the line numbers $sMaxLen = (strlen(''.count($aData)) < 3) ? 3 : strlen(''.count($aData)); // Pad line numbers to the appropriate number of chars, but at least 3 - + // Compute the list of search/reconciliation criteria $aSearchKeys = array(); foreach($aSearchFields as $index => $sDummy) @@ -303,16 +304,16 @@ try { } else { - $aSearchKeys[$sSearchField] = ''; + $aSearchKeys[$sSearchField] = ''; } if (!MetaModel::IsValidFilterCode($sClassName, $sSearchField)) { // Remove invalid or unmapped search fields $aSearchFields[$index] = null; - unset($aSearchKeys[$sSearchField]); + unset($aSearchKeys[$sSearchField]); } } - + // Compute the list of fields and external keys to process $aExtKeys = array(); $aAttributes = array(); @@ -345,13 +346,13 @@ try { } else { - $aAttributes[$sAttCode] = $iIndex; + $aAttributes[$sAttCode] = $iIndex; } } } - } + } } - + $oMyChange = null; if (!$bSimulate) { @@ -360,7 +361,7 @@ try { CMDBObject::SetCurrentChangeFromParams($sUserString, CMDBChangeOrigin::CSV_INTERACTIVE); $oMyChange = CMDBObject::GetCurrentChange(); } - + $oBulk = new BulkChange( $sClassName, $aData, @@ -370,7 +371,7 @@ try { empty($sSynchroScope) ? null : $sSynchroScope, $aSynchroUpdate, $sChosenDateFormat, // date format - true // localize + true // localize ); $oBulk->SetReportHtml(); @@ -437,7 +438,6 @@ try { case 'RowStatus_NewObj': $iCreated++; - $sFinalClass = $aResRow['finalclass']; $sStatus = ''; $sCSSRowClass = 'ibo-csv-import--row-added'; if ($bSimulate) { @@ -453,7 +453,7 @@ try { case 'RowStatus_Issue': $iErrors++; $sMessage .= GetDivAlert($oStatus->GetDescription()); - $sStatus = '';//translate + $sStatus = '
';//translate $sCSSMessageClass = 'ibo-csv-import--cell-error'; $sCSSRowClass = 'ibo-csv-import--row-error'; if (array_key_exists($iLine, $aData)) { @@ -474,33 +474,36 @@ try { if (isset($aExternalKeysByColumn[$iNumber - 1])) { $sExtKeyName = $aExternalKeysByColumn[$iNumber - 1]; $oExtKeyCellStatus = $aResRow[$sExtKeyName]; - switch (get_class($oExtKeyCellStatus)) { - case 'CellStatus_Issue': - case 'CellStatus_SearchIssue': - case 'CellStatus_NullIssue': - case 'CellStatus_Ambiguous': - $sCellMessage .= GetDivAlert($oExtKeyCellStatus->GetDescription()); - break; - - default: - // Do nothing - } + $oCellStatus = $oExtKeyCellStatus; } $sHtmlValue = $oCellStatus->GetDisplayableValue(); switch (get_class($oCellStatus)) { case 'CellStatus_Issue': + case 'CellStatus_NullIssue': $sCellMessage .= GetDivAlert($oCellStatus->GetDescription()); $aTableRow[$sClassName.'/'.$sAttCode] = '
'.Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue).$sCellMessage.'
'; break; case 'CellStatus_SearchIssue': - $sCellMessage .= GetDivAlert($oCellStatus->GetDescription()); - $aTableRow[$sClassName.'/'.$sAttCode] = '
ERROR: '.$sHtmlValue.$sCellMessage.'
'; + $aTableRow[$sClassName.'/'.$sAttCode] = sprintf("%s%s%s%s%s%s", + '
', + Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue), + GetDivAlert($oCellStatus->GetDescription()), + '
' + ); break; case 'CellStatus_Ambiguous': - $sCellMessage .= GetDivAlert($oCellStatus->GetDescription()); - $aTableRow[$sClassName.'/'.$sAttCode] = '
'.Dict::Format('UI:CSVReport-Object-Ambiguous', $sHtmlValue).$sCellMessage.'
'; + $aTableRow[$sClassName.'/'.$sAttCode] = sprintf("%s%s%s%s%s%s", + '
', + Dict::Format('UI:CSVReport-Object-Ambiguous', $sHtmlValue), + GetDivAlert($oCellStatus->GetDescription()), + '
' + ); break; case 'CellStatus_Modify': @@ -589,7 +592,7 @@ try { $oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxUnchanged)); $oPage->add_ready_script("$('#show_created').on('click', function(){ToggleRows('ibo-csv-import--row-added')})"); - $oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel(' '.sprintf($aDisplayFilters['errors'], $iErrors), '', "1", "show_errors", "checkbox"); + $oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel(' '.sprintf($aDisplayFilters['errors'], $iErrors).'', '', "1", "show_errors", "checkbox"); $oCheckBoxUnchanged->GetInput()->SetIsChecked(true); $oCheckBoxUnchanged->SetBeforeInput(false); $oCheckBoxUnchanged->GetInput()->AddCSSClass('ibo-input-checkbox'); @@ -676,7 +679,7 @@ try { EOF ); } - + $sErrors = json_encode(Dict::Format('UI:CSVImportError_items', $iErrors)); $sCreated = json_encode(Dict::Format('UI:CSVImportCreated_items', $iCreated)); $sModified = json_encode(Dict::Format('UI:CSVImportModified_items', $iModified)); @@ -771,7 +774,7 @@ EOF { return null; } - + } /** * Perform the actual load of the CSV data and display the results @@ -795,7 +798,7 @@ EOF $oField->AddSubBlock($oText); } } - + /** * Simulate the load of the CSV data and display the results * @param WebPage $oPage The web page to display the wizard @@ -807,7 +810,7 @@ EOF $oPage->AddSubBlock($oPanel); ProcessCSVData($oPage, true /* simulate */); } - + /** * Select the mapping between the CSV column and the fields of the objects * @param WebPage $oPage The web page to display the wizard @@ -920,10 +923,10 @@ EOF $aSearchFields = utils::ReadParam('search_field', array(), false, 'field_name'); $sFieldsMapping = addslashes(json_encode($aFieldsMapping)); $sSearchFields = addslashes(json_encode($aSearchFields)); - + $oPage->add_ready_script("DoMapping('$sFieldsMapping', '$sSearchFields');"); // There is already a class selected, run the mapping } - + $oPage->add_script( <<add_ready_script('$("#CSVImportHistory table.listResults").tableHover();'); - $oPage->add_ready_script('$("#CSVImportHistory table.listResults").tablesorter( { widgets: ["myZebra", "truncatedList"]} );'); + $oPage->add_ready_script('$("#CSVImportHistory table.listResults").tablesorter( { widgets: ["myZebra", "truncatedList"]} );'); break; - + case 10: // Case generated by BulkChange::DisplayImportHistory $iChange = (int)utils::ReadParam('changeid', 0); BulkChange::DisplayImportHistoryDetails($oPage, $iChange); break; - + case 5: LoadData($oPage); break; - + case 4: Preview($oPage); break; - + case 3: SelectMapping($oPage); break; - + case 2: SelectOptions($oPage); break; - + case 1: case 6: // Loop back here when we are done default: Welcome($oPage); } - + $oPage->output(); } catch(CoreException $e) { require_once(APPROOT.'/setup/setuppage.class.inc.php'); $oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError')); - $oP->add("

".Dict::S('UI:FatalErrorMessage')."

\n"); - $oP->error(Dict::Format('UI:Error_Details', $e->getHtmlDesc())); + $oP->add("

".Dict::S('UI:FatalErrorMessage')."

\n"); + $oP->error(Dict::Format('UI:Error_Details', $e->getHtmlDesc())); $oP->output(); if (MetaModel::IsLogEnabledIssue()) @@ -1680,8 +1683,8 @@ catch(Exception $e) { require_once(APPROOT.'/setup/setuppage.class.inc.php'); $oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError')); - $oP->add("

".Dict::S('UI:FatalErrorMessage')."

\n"); - $oP->error(Dict::Format('UI:Error_Details', $e->getMessage())); + $oP->add("

".Dict::S('UI:FatalErrorMessage')."

\n"); + $oP->error(Dict::Format('UI:Error_Details', $e->getMessage())); $oP->output(); if (MetaModel::IsLogEnabledIssue()) @@ -1701,4 +1704,4 @@ catch(Exception $e) IssueLog::Error($e->getMessage()); } -} \ No newline at end of file +} diff --git a/test/ItopDataTestCase.php b/test/ItopDataTestCase.php index c67786dd8..63a82b11a 100644 --- a/test/ItopDataTestCase.php +++ b/test/ItopDataTestCase.php @@ -72,6 +72,7 @@ define('TAG_ATTCODE', 'domains'); class ItopDataTestCase extends ItopTestCase { private $iTestOrgId; + // For cleanup private $aCreatedObjects = array(); @@ -885,7 +886,7 @@ class ItopDataTestCase extends ItopTestCase } /** - * Import a consistent set of iTop objects from the specified XML text string + * Import a consistent set of iTop objects from the specified XML text string * @param string $sXmlDataset * @param boolean $bSearch If true, a search will be performed on each object (based on its reconciliation keys) * before trying to import it (existing objects will be updated) diff --git a/test/core/BulkChangeExtKeyTest.inc.php b/test/core/BulkChangeExtKeyTest.inc.php new file mode 100644 index 000000000..ebc7f9a16 --- /dev/null +++ b/test/core/BulkChangeExtKeyTest.inc.php @@ -0,0 +1,344 @@ +Count(); + if ($iCount != 0){ + while ($oRack = $oSet->Fetch()){ + $oRack->DBDelete(); + } + } + } + + /** + * @dataProvider ReconciliationKeyProvider + */ + public function testExternalFieldIssueImportFail_NoObjectAtAll($bIsRackReconKey){ + $this->deleteAllRacks(); + + $this->performBulkChangeTest( + 'There are no \'Rack\' objects', + "", + null, + $bIsRackReconKey + ); + } + + public function createRackObjects($aRackDict) { + foreach ($aRackDict as $iOrgId => $aRackNames) { + foreach ($aRackNames as $sRackName) { + $this->createObject('Rack', ['name' => $sRackName, 'description' => "${sRackName}Desc", 'org_id' => $iOrgId]); + } + } + } + + private function createAnotherUserInAnotherOrg() { + $oOrg2 = $this->CreateOrganization('UnitTestOrganization2'); + $oProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'Configuration Manager'), true); + + $sUid = $this->GetUid(); + + $oUserProfile = new \URP_UserProfile(); + $oUserProfile->Set('profileid', $oProfile->GetKey()); + $oUserProfile->Set('reason', 'UNIT Tests'); + $oSet = \DBObjectSet::FromObject($oUserProfile); + + $oPerson = $this->CreatePerson('666', $oOrg2->GetKey()); + $oUser = $this->createObject('UserLocal', array( + 'contactid' => $oPerson->GetKey(), + 'login' => $sUid, + 'password' => "ABCdef$sUid@12345", + 'language' => 'EN US', + 'profile_list' => $oSet, + )); + + $oAllowedOrgList = $oUser->Get('allowed_org_list'); + /** @var \URP_UserOrg $oUserOrg */ + $oUserOrg = \MetaModel::NewObject('URP_UserOrg', ['allowed_org_id' => $oOrg2->GetKey(),]); + $oAllowedOrgList->AddItem($oUserOrg); + $oUser->Set('allowed_org_list', $oAllowedOrgList); + $oUser->DBWrite(); + return [$oOrg2, $oUser]; + } + + public function ReconciliationKeyProvider(){ + return [ + 'rack_id NOT a reconcilication key' => [ false ], + 'rack_id reconcilication key' => [ true ], + ]; + } + + + /** + * @dataProvider ReconciliationKeyProvider + */ + public function testExternalFieldIssueImportFail_NoObjectVisibleByCurrentUser($bIsRackReconKey){ + $this->deleteAllRacks(); + $this->createRackObjects( + [ + $this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4'] + ] + ); + + list($oOrg2, $oUser) = $this->createAnotherUserInAnotherOrg(); + \UserRights::Login($oUser->Get('login')); + + $this->performBulkChangeTest( + "There are no 'Rack' objects found with your current profile", + "", + $oOrg2, + $bIsRackReconKey + ); + } + + /** + * @dataProvider ReconciliationKeyProvider + */ + public function testExternalFieldIssueImportFail_SomeObjectVisibleByCurrentUser($bIsRackReconKey){ + $this->deleteAllRacks(); + list($oOrg2, $oUser) = $this->createAnotherUserInAnotherOrg(); + $this->createRackObjects( + [ + $this->getTestOrgId() => ['RackTest1', 'RackTest2'], + $oOrg2->GetKey() => ['RackTest3', 'RackTest4'], + ] + ); + + \UserRights::Login($oUser->Get('login')); + + $this->performBulkChangeTest( + "There are some 'Rack' objects not visible with your current profile", + "Some possible 'Rack' value(s): RackTest3, RackTest4", + $oOrg2, + $bIsRackReconKey + ); + } + + /** + * @dataProvider ReconciliationKeyProvider + */ + public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser($bIsRackReconKey){ + $this->deleteAllRacks(); + $this->createRackObjects( + [ + $this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4'] + ] + ); + + $this->performBulkChangeTest( + "No match for value 'UnexistingRack'", + "Some possible 'Rack' value(s): RackTest1, RackTest2, RackTest3...", + null, + $bIsRackReconKey + ); + } + + /** + * @dataProvider ReconciliationKeyProvider + */ + public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser_AmbigousMatch($bIsRackReconKey){ + $this->deleteAllRacks(); + $this->createRackObjects( + [ + $this->getTestOrgId() => ['UnexistingRack', 'UnexistingRack'] + ] + ); + + $this->performBulkChangeTest( + "invalid value for attribute", + "Ambiguous: found 2 objects", + null, + $bIsRackReconKey, + null, + null, + null, + 'Found 2 matches' + ); + } + + + /** + * @dataProvider ReconciliationKeyProvider + */ + public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser_FurtherExtKeyForRack($bIsRackReconKey){ + $this->deleteAllRacks(); + $this->createRackObjects( + [ + $this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4'] + ] + ); + + $aCsvData = [["UnexistingRackDescription"]]; + $aExtKeys = ["org_id" => ["name" => 0], "rack_id" => ["name" => 1, "description" => 3]]; + + $sSearchLinkUrl = 'UI.php?operation=search&filter=%5B%22SELECT+%60Rack%60+FROM+Rack+AS+%60Rack%60+WHERE+%28%28%60Rack%60.%60name%60+%3D+%3Aname%29+AND+%28%60Rack%60.%60description%60+%3D+%3Adescription%29%29%22%2C%7B%22name%22%3A%22UnexistingRack%22%2C%22description%22%3A%22UnexistingRackDescription%22%7D%2C%5B%5D%5D' +; + $this->performBulkChangeTest( + "No match for value 'UnexistingRack UnexistingRackDescription'", + "Some possible 'Rack' value(s): RackTest1 RackTest1Desc, RackTest2 RackTest2Desc, RackTest3 RackTest3Desc...", + null, + $bIsRackReconKey, + $aCsvData, + $aExtKeys, + $sSearchLinkUrl + ); + } + + + private function GetUid(){ + if (is_null($this->sUid)){ + $this->sUid = date('dmYHis'); + } + + return $this->sUid; + } + + /** * + * @param $aInitData + * @param $aCsvData + * @param $aAttributes + * @param $aExtKeys + * @param $aReconcilKeys + */ + public function performBulkChangeTest($sExpectedDisplayableValue, $sExpectedDescription, $oOrg, $bIsRackReconKey, + $aAdditionalCsvData=null, $aExtKeys=null, $sSearchLinkUrl=null, $sError="Object not found") { + if ($sSearchLinkUrl===null){ + $sSearchLinkUrl = 'UI.php?operation=search&filter=%5B%22SELECT+%60Rack%60+FROM+Rack+AS+%60Rack%60+WHERE+%28%60Rack%60.%60name%60+%3D+%3Aname%29%22%2C%7B%22name%22%3A%22UnexistingRack%22%7D%2C%5B%5D%5D'; + } + if (is_null($oOrg)){ + $iOrgId = $this->getTestOrgId(); + $sOrgName = "UnitTestOrganization"; + }else{ + $iOrgId = $oOrg->GetKey(); + $sOrgName = $oOrg->Get('name'); + } + + $sUid = $this->GetUid(); + + $aCsvData = [[$sOrgName, "UnexistingRack", "$sUid"]]; + if ($aAdditionalCsvData !== null){ + foreach ($aAdditionalCsvData as $i => $aData){ + foreach ($aData as $sData){ + $aCsvData[$i][] = $sData; + } + } + } + $aAttributes = ["name" => 2]; + if ($aExtKeys == null){ + $aExtKeys = ["org_id" => ["name" => 0], "rack_id" => ["name" => 1]]; + } + $aReconcilKeys = [ "name" ]; + + $aResult = [ + 0 => $sOrgName, + "org_id" => $iOrgId, + 1 => "UnexistingRack", + 2 => "\"$sUid\"", + "rack_id" => [ + $sExpectedDisplayableValue, + $sExpectedDescription + ], + "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => $sError, + ]; + + if ($bIsRackReconKey){ + $aReconcilKeys[] = "rack_id"; + $aResult[2] = $sUid; + $aResult["__STATUS__"] = "Issue: failed to reconcile"; + } + + + CMDBSource::Query('START TRANSACTION'); + //change value during the test + $db_core_transactions_enabled=MetaModel::GetConfig()->Get('db_core_transactions_enabled'); + MetaModel::GetConfig()->Set('db_core_transactions_enabled',false); + + $this->debug("aCsvData:".json_encode($aCsvData[0])); + $this->debug("aReconcilKeys:". var_export($aReconcilKeys)); + $oBulk = new \BulkChange( + "Server", + $aCsvData, + $aAttributes, + $aExtKeys, + $aReconcilKeys, + null, + null, + "Y-m-d H:i:s", // date format + true // localize + ); + $this->debug("BulkChange:"); + $oChange = \CMDBObject::GetCurrentChange(); + $this->debug("GetCurrentChange:"); + $aRes = $oBulk->Process($oChange); + $this->debug("Process:"); + static::assertNotNull($aRes); + $this->debug("assertNotNull:"); + var_dump($aRes); + foreach ($aRes as $aRow) { + if (array_key_exists('__STATUS__', $aRow)) { + $sStatus = $aRow['__STATUS__']; + $this->debug("sStatus:".$sStatus->GetDescription()); + $this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription()); + foreach ($aRow as $i => $oCell) { + if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") { + $this->debug("i:".$i); + $this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue()); + if (array_key_exists($i,$aResult)) { + $this->debug("aResult:".var_export($aResult[$i])); + if ($oCell instanceof \CellStatus_SearchIssue || + $oCell instanceof \CellStatus_Ambiguous) { + $this->assertEquals($aResult[$i][0], $oCell->GetDisplayableValue(), + "failure on ".get_class($oCell).' cell type'); + $this->assertEquals($sSearchLinkUrl, $oCell->GetSearchLinkUrl(), + "failure on ".get_class($oCell).' cell type'); + $this->assertEquals($aResult[$i][1], $oCell->GetDescription(), + "failure on ".get_class($oCell).' cell type'); + } + } + } else if ($i === "__ERRORS__") { + $sErrors = array_key_exists("__ERRORS__", $aResult) ? $aResult["__ERRORS__"] : ""; + $this->assertEquals( $sErrors, $oCell->GetDescription()); + } + } + $this->assertEquals( $aResult[0], $aRow[0]->GetDisplayableValue()); + } + } + MetaModel::GetConfig()->Set('db_core_transactions_enabled',$db_core_transactions_enabled); + } +} diff --git a/test/core/BulkChangeTest.inc.php b/test/core/BulkChangeTest.inc.php index 9d2dd1b99..25c72d6f6 100644 --- a/test/core/BulkChangeTest.inc.php +++ b/test/core/BulkChangeTest.inc.php @@ -104,13 +104,13 @@ class BulkChangeTest extends ItopDataTestCase { if (array_key_exists('__STATUS__', $aRow)) { $sStatus = $aRow['__STATUS__']; //$this->debug("sStatus:".$sStatus->GetDescription()); - $this->assertEquals($sStatus->GetDescription(), $aResult["__STATUS__"]); + $this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription()); foreach ($aRow as $i => $oCell) { - if ($i != "finalclass" && $i != "__STATUS__") { + if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") { $this->debug("i:".$i); $this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue()); $this->debug("aResult:".$aResult[$i]); - $this->assertEquals($oCell->GetDisplayableValue(), $aResult[$i]); + $this->assertEquals($aResult[$i], $oCell->GetDisplayableValue()); } } } @@ -131,28 +131,37 @@ class BulkChangeTest extends ItopDataTestCase { ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], ["org_id" => ["name" => 0]], ["id"], - [ 0 => "Demo", "org_id" => "n/a", 1 => "Server1", 2 => "1", 3 => "production", 4 => "date", "id" => 1, "__STATUS__" => "Issue: wrong date format"], + [ 0 => "Demo", "org_id" => "n/a", 1 => "Server1", 2 => "1", 3 => "production", 4 => "'date' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"], ], "Case 1 : no match" => [ [["Bad", "Server1", "1", "production", ""]], ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], ["org_id" => ["name" => 0]], ["id"], - ["org_id" => "",1 => "Server1",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], + ["org_id" => "No match for value 'Bad'",1 => "Server1",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], ], "Case 10 : Missing mandatory value" => [ [["", "Server1", "1", "production", ""]], ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], ["org_id" => ["name" => 0]], ["id"], - [ "org_id" => "", 1 => "Server1", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], + [ "org_id" => "invalid value for attribute", 1 => "Server1", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], ], "Case 6 : Unexpected value" => [ [["Demo", "Server1", "1", "", ""]], ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], ["org_id" => ["name" => 0]], ["id"], - [0 => "Demo", "org_id" => "3", 1 => "Server1", 2 => "1", 3 => "<svg onclick"alert(1)">", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], + [ + 0 => "Demo", + "org_id" => "3", + 1 => "Server1", + 2 => "1", + 3 => "'<svg onclick"alert(1)">' is an invalid value", + 4 => "", + "id" => 1, + "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Unexpected value for attribute 'status': no match found, check spelling"], ], ]; } @@ -172,17 +181,20 @@ class BulkChangeTest extends ItopDataTestCase { //change value during the test $db_core_transactions_enabled=MetaModel::GetConfig()->Get('db_core_transactions_enabled'); MetaModel::GetConfig()->Set('db_core_transactions_enabled',false); - /** @var Server $oServer */ - $oServer = $this->createObject('Server', array( - 'name' => $aInitData[1], - 'status' => $aInitData[2], - 'org_id' => $aInitData[0], - 'purchase_date' => $aInitData[3], - )); - $aCsvData[0][2]=$oServer->GetKey(); - $aResult[2]=$oServer->GetKey(); - $aResult["id"]=$oServer->GetKey(); - $this->debug("oServer->GetKey():".$oServer->GetKey()); + + if (is_array($aInitData) && sizeof($aInitData) != 0) { + /** @var Server $oServer */ + $oServer = $this->createObject('Server', array( + 'name' => $aInitData[1], + 'status' => $aInitData[2], + 'org_id' => $aInitData[0], + 'purchase_date' => $aInitData[3], + )); + $aCsvData[0][2]=$oServer->GetKey(); + $aResult[2]=$oServer->GetKey(); + $aResult["id"]=$oServer->GetKey(); + $this->debug("oServer->GetKey():".$oServer->GetKey()); + } $this->debug("aCsvData:".json_encode($aCsvData[0])); $this->debug("aReconcilKeys:".$aReconcilKeys[0]); $oBulk = new \BulkChange( @@ -207,13 +219,16 @@ class BulkChangeTest extends ItopDataTestCase { if (array_key_exists('__STATUS__', $aRow)) { $sStatus = $aRow['__STATUS__']; $this->debug("sStatus:".$sStatus->GetDescription()); - $this->assertEquals($sStatus->GetDescription(), $aResult["__STATUS__"]); + $this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription()); foreach ($aRow as $i => $oCell) { - if ($i != "finalclass" && $i != "__STATUS__") { + if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") { $this->debug("i:".$i); $this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue()); $this->debug("aResult:".$aResult[$i]); - $this->assertEquals( $aResult[$i], $oCell->GetDisplayableValue()); + $this->assertEquals( $aResult[$i], $oCell->GetDisplayableValue(), "failure on " . get_class($oCell) . ' cell type'); + } else if ($i === "__ERRORS__") { + $sErrors = array_key_exists("__ERRORS__", $aResult) ? $aResult["__ERRORS__"] : ""; + $this->assertEquals( $sErrors, $oCell->GetDescription()); } } $this->assertEquals( $aResult[0], $aRow[0]->GetDisplayableValue()); @@ -225,21 +240,58 @@ class BulkChangeTest extends ItopDataTestCase { public function CSVImportProvider() { return [ - "Case 6 - 1 : Unexpected value" => [ + "Case 6 - 1 : Unexpected value (update)" => [ ["1", "ServerTest", "production", ""], [["Demo", "ServerTest", "key", "BadValue", ""]], ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], ["org_id" => ["name" => 0]], ["id"], - [0 => "Demo", "org_id" => "3", 1 => "ServerTest", 2 => "1", 3 => "BadValue", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], + [ + 0 => "Demo", + "org_id" => "3", + 1 => "ServerTest", + 2 => "1", + 3 => "'BadValue' is an invalid value", + 4 => "", + "id" => 1, + "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Allowed 'status' value(s): implementation,obsolete,production,stock", + ], ], - "Case 6 - 2 : Unexpected value" => [ + "Case 6 - 2 : Unexpected value (update)" => [ ["1", "ServerTest", "production", ""], [["Demo", "ServerTest", "key", "", ""]], ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], ["org_id" => ["name" => 0]], ["id"], - [0 => "Demo", "org_id" => "3", 1 => "ServerTest", 2 => "1", 3 => "<svg onclick"alert(1)">", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], + [ + 0 => "Demo", + "org_id" => "3", + 1 => "ServerTest", + 2 => "1", + 3 => "'<svg onclick"alert(1)">' is an invalid value", + 4 => "", + "id" => 1, + "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Allowed 'status' value(s): implementation,obsolete,production,stock", + ], + ], + "Case 6 - 3 : Unexpected value (creation)" => [ + [], + [["Demo", "ServerTest", "", ""]], + ["name" => 1, "status" => 2, "purchase_date" => 3], + ["org_id" => ["name" => 0]], + ["name"], + [ + 0 => "Demo", + "org_id" => "3", + 1 => "\"ServerTest\"", + 2 => "'<svg onclick"alert(1)">' is an invalid value", + 3 => "", + "id" => 1, + "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Allowed 'status' value(s): implementation,obsolete,production,stock", + ], ], "Case 8 : unchanged name" => [ ["1", "", "production", ""], @@ -263,7 +315,7 @@ class BulkChangeTest extends ItopDataTestCase { ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], ["org_id" => ["name" => 0]], ["id"], - [ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "date", "id" => 1, "__STATUS__" => "Issue: wrong date format"], + [ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "'date' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"], ], "Case 9 - 2: wrong date format" => [ ["1", "ServerTest", "production", ""], @@ -271,7 +323,7 @@ class BulkChangeTest extends ItopDataTestCase { ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], ["org_id" => ["name" => 0]], ["id"], - [ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "<svg onclick"alert(1)">", "id" => 1, "__STATUS__" => "Issue: wrong date format"], + [ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "'<svg onclick"alert(1)">' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"], ], "Case 1 - 1 : no match" => [ ["1", "ServerTest", "production", ""], @@ -279,7 +331,9 @@ class BulkChangeTest extends ItopDataTestCase { ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], ["org_id" => ["name" => 0]], ["id"], - [ 0 => "Bad", "org_id" => "",1 => "ServerTest",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], + [ 0 => "Bad", "org_id" => "No match for value 'Bad'",1 => "ServerTest",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Object not found", + ], ], "Case 1 - 2 : no match" => [ ["1", "ServerTest", "production", ""], @@ -287,7 +341,9 @@ class BulkChangeTest extends ItopDataTestCase { ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], ["org_id" => ["name" => 0]], ["id"], - [ 0 => "<svg fonclick"alert(1)">", "org_id" => "",1 => "ServerTest",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], + [ 0 => "<svg fonclick"alert(1)">", "org_id" => "No match for value ''",1 => "ServerTest",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Object not found", + ], ], "Case 10 : Missing mandatory value" => [ ["1", "ServerTest", "production", ""], @@ -295,7 +351,9 @@ class BulkChangeTest extends ItopDataTestCase { ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], ["org_id" => ["name" => 0]], ["id"], - [ 0 => "", "org_id" => "", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], + [ 0 => "", "org_id" => "invalid value for attribute", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Null not allowed", + ], ], "Case 0 : Date format" => [ @@ -304,7 +362,7 @@ class BulkChangeTest extends ItopDataTestCase { ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], ["org_id" => ["name" => 0]], ["id"], - [ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "2020-20-03", "id" => 1, "__STATUS__" => "Issue: wrong date format"], + [ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "'2020-20-03' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"], ], ]; } @@ -326,20 +384,22 @@ class BulkChangeTest extends ItopDataTestCase { //change value during the test $db_core_transactions_enabled=MetaModel::GetConfig()->Get('db_core_transactions_enabled'); MetaModel::GetConfig()->Set('db_core_transactions_enabled',false); - /** @var Server $oServer */ - $oOrganisation = $this->createObject('Organization', array( - 'name' =>$aInitData[0] - )); - $aResult["org_id"]=$oOrganisation->GetKey(); - $oServer = $this->createObject('Server', array( - 'name' => $aInitData[1], - 'status' => $aInitData[2], - 'org_id' => $oOrganisation->GetKey(), - 'purchase_date' => $aInitData[3], - )); - $aCsvData[0][2]=$oServer->GetKey(); - $aResult[2]=$oServer->GetKey(); - $aResult["id"]=$oServer->GetKey(); + if (is_array($aInitData) && sizeof($aInitData) != 0) { + /** @var Server $oServer */ + $oOrganisation = $this->createObject('Organization', array( + 'name' => $aInitData[0] + )); + $aResult["org_id"] = $oOrganisation->GetKey(); + $oServer = $this->createObject('Server', array( + 'name' => $aInitData[1], + 'status' => $aInitData[2], + 'org_id' => $oOrganisation->GetKey(), + 'purchase_date' => $aInitData[3], + )); + $aCsvData[0][2]=$oServer->GetKey(); + $aResult[2]=$oServer->GetKey(); + $aResult["id"]=$oServer->GetKey(); + } $oBulk = new \BulkChange( "Server", $aCsvData, @@ -356,15 +416,17 @@ class BulkChangeTest extends ItopDataTestCase { static::assertNotNull($aRes); foreach ($aRes as $aRow) { foreach ($aRow as $i => $oCell) { - if ($i != "finalclass" && $i != "__STATUS__") { + if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") { $this->debug("i:".$i); $this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue()); $this->debug("aResult:".$aResult[$i]); $this->assertEquals($aResult[$i], $oCell->GetDisplayableValue()); - } - elseif ($i == "__STATUS__") { + } elseif ($i == "__STATUS__") { $sStatus = $aRow['__STATUS__']; $this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription()); + } else if ($i === "__ERRORS__") { + $sErrors = array_key_exists("__ERRORS__", $aResult) ? $aResult["__ERRORS__"] : ""; + $this->assertEquals( $sErrors, $oCell->GetDescription()); } } $this->assertEquals($aResult[0], $aRow[0]->GetDisplayableValue()); @@ -402,4 +464,4 @@ class BulkChangeTest extends ItopDataTestCase { ]; } -} \ No newline at end of file +} diff --git a/test/integration/DictionariesConsistencyTest.php b/test/integration/DictionariesConsistencyTest.php index fbc8ef031..7b381857e 100644 --- a/test/integration/DictionariesConsistencyTest.php +++ b/test/integration/DictionariesConsistencyTest.php @@ -17,6 +17,7 @@ namespace Combodo\iTop\Test\UnitTest\Integration; use Combodo\iTop\Test\UnitTest\ItopTestCase; +use Dict; class DictionariesConsistencyTest extends ItopTestCase { @@ -148,4 +149,67 @@ class DictionariesConsistencyTest extends ItopTestCase $sMessage = "File `{$sDictFile}` syntax didn't matched expectations\nparsing results=".var_export($output, true); self::assertEquals($bIsSyntaxValid, $bDictFileSyntaxOk, $sMessage); } + + /** + * @dataProvider ImBulChanportCsvMessageStillOkProvider + * make sure N°5305 dictionary changes are still here and UI remains unbroken for any lang + */ + public function testImportCsvMessageStillOk($sLangCode, $sDictFile) + { + $aFailedLabels = []; + $aLabelsToTest = [ + 'UI:CSVReport-Value-SetIssue' => [], + 'UI:CSVReport-Value-ChangeIssue' => [ 'arg1' ], + 'UI:CSVReport-Value-NoMatch' => [ 'arg1' ], + 'UI:CSVReport-Value-NoMatch-PossibleValues' => [ 'arg1', 'arg2' ], + 'UI:CSVReport-Value-NoMatch-NoObject' => [ 'arg1' ], + 'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => [ 'arg1' ], + 'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => [ 'arg1' ], + ]; + + $sLanguageCode = strtoupper(str_replace('-', ' ', $sLangCode)); + require_once(APPROOT.'env-'.\utils::GetCurrentEnvironment().'/dictionaries/languages.php'); + Dict::SetUserLanguage($sLanguageCode); + foreach ($aLabelsToTest as $sLabelKey => $aLabelArgs){ + try{ + $sLabelValue = Dict::Format($sLabelKey, ...$aLabelArgs); + var_dump($sLabelValue); + } catch (\Exception $e){ + $aFailedLabels[] = $sLabelKey; + + var_dump([ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + 'label_name' => $sLabelKey, + 'label_args' =>$aLabelArgs, + ]); + } + } + $this->assertEquals([], $aFailedLabels, "test fail for lang $sLangCode and labels (" . implode(",", $aFailedLabels) . ')'); + } + + public function ImportCsvMessageStillOkProvider(){ + return $this->GetDictFiles(); + } + + /** + * return a map linked to *.dict.php files that are generated after setup + * each entry key is lang code (example 'en') + * each value is an array with lang code (again) and dict file path + * @return array + */ + private function GetDictFiles() : array { + $aDictFiles = []; + + foreach (glob(APPROOT.'env-'.\utils::GetCurrentEnvironment().'/dictionaries/*.dict.php') as $sDictFile){ + if (preg_match('/.*\\/(.*).dict.php/', $sDictFile, $aMatches)){ + $sLangCode = $aMatches[1]; + $aDictFiles[$sLangCode] = [ + 'lang' => $sLangCode, + 'file' => $sDictFile + ]; + } + } + return $aDictFiles; + } } diff --git a/test/testlist.inc.php b/test/testlist.inc.php index 9a2851d8f..23b7b3c69 100644 --- a/test/testlist.inc.php +++ b/test/testlist.inc.php @@ -848,89 +848,6 @@ $oSearch->AddCondition_PointingTo($oOrgSearch, "org_id"); } } -/////////////////////////////////////////////////////////////////////////// -// Test bulk load API -/////////////////////////////////////////////////////////////////////////// - -class TestItopBulkLoad extends TestBizModel -{ - static public function GetName() - { - return 'Itop - test BulkChange class'; - } - - static public function GetDescription() - { - return 'Execute a bulk change at the Core API level'; - } - - protected function DoExecute() - { - $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('_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( - 'UserLocal', - $aData, - // attributes - array('login' => '_login', 'password' => '_password', 'profile_list' => '_profiles'), - // ext keys - array('contactid' => array('name' => '_contact_name')), - // reconciliation - array('login'), - // Synchro - scope - "SELECT UserLocal", - // Synchro - set attribute on missing objects - array ('password' => 'terminated', 'login' => 'terminated'.time()) - ); - - if (false) - { - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $oMyChange->Set("userinfo", "Testor"); - $iChangeId = $oMyChange->DBInsert(); -// echo "Created new change: $iChangeId
"; - } - - echo "

Planned for loading...

"; - $aRes = $oBulk->Process(); - self::DumpVariable($aRes); - if (false) - { - echo "

Go for loading...

"; - $aRes = $oBulk->Process($oMyChange); - self::DumpVariable($aRes); - } - - return; - } -} - - /////////////////////////////////////////////////////////////////////////// // Test data load /////////////////////////////////////////////////////////////////////////// diff --git a/test/webservices/ImportTest.inc.php b/test/webservices/ImportTest.inc.php new file mode 100644 index 000000000..0f10350c8 --- /dev/null +++ b/test/webservices/ImportTest.inc.php @@ -0,0 +1,198 @@ +sTmpFile) && is_file($this->sTmpFile)){ + unlink($this->sTmpFile); + } + } + + protected function setUp() : void{ + parent::setUp(); + + $this->sTmpFile = tempnam(sys_get_temp_dir(), 'import_csv_'); + + require_once(APPROOT.'application/startup.inc.php'); + $this->sUid = date('dmYHis'); + $this->sLogin = "import-" .$this->sUid; + $this->oOrg = $this->CreateOrganization($this->sUid); + + $sConfigFile = \utils::GetConfig()->GetLoadedFile(); + @chmod($sConfigFile, 0770); + $this->sUrl = \MetaModel::GetConfig()->Get('app_root_url'); + @chmod($sConfigFile, 0444); // Read-only + + $oRestProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'REST Services User'), true); + $oAdminProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'Administrator'), true); + + if (is_object($oRestProfile) && is_object($oAdminProfile)) + { + $oUser = $this->CreateUser($this->sLogin, $oRestProfile->GetKey(), $this->sPassword); + $this->AddProfileToUser($oUser, $oAdminProfile->GetKey()); + } else { + throw new \Exception("setup failed. test cannot work as usual"); + } + } + + public function ImportOkProvider(){ + return [ + 'with reconciliation key' => [ "sReconciliationKeys" => "name,first_name,org_id->name" ], + 'without reconciliation key' => [ "sReconciliationKeys" => null ], + ]; + } + /** + * @dataProvider ImportOkProvider + */ + public function testImportOk($sReconciliationKeys){ + $sFirstName = "firstname_UID"; + $sLastName = "lastname_UID"; + $sEmail = "email_UID@toto.fr"; + + $this->performImportTesting( + '"first_name","name", "email", "org_id->name"', + sprintf('"%s", "%s", "%s", UID', $sFirstName, $sLastName, $sEmail), + sprintf('ORGID;"%s";"%s";"%s"', $sFirstName, $sLastName, $sEmail), + $sReconciliationKeys, + 0, + 1 + ); + } + + public function ImportFailProvider(){ + return [ + 'without reconciliation key' => [ + "sReconciliationKeys" => null, + "sExpectedLastLineNeedle" => 'Issue: Unexpected attribute value(s);n/a;n/a;No match for value \'gabuzomeu\'. Some possible \'Organization\' value(s): ' + ], + 'with reconciliation key' => [ + "sReconciliationKeys" => "name,first_name,org_id->name", + "sExpectedLastLineNeedle" => 'Issue: failed to reconcile;n/a;n/a;No match for value \'gabuzomeu\'. Some possible \'Organization\' value(s): ' + ], + ]; + } + /** + * @dataProvider ImportFailProvider + */ + + public function testImportFail_ExternalKey($sReconciliationKeys, $sExpectedLastLineNeedle){ + $sFirstName = "firstname_UID"; + $sLastName = "lastname_UID"; + $sEmail = "email_UID@toto.fr"; + + $this->performImportTesting( + '"first_name","name", "email", "org_id->name"', + sprintf('"%s", "%s", "%s", gabuzomeu', $sFirstName, $sLastName, $sEmail), + $sExpectedLastLineNeedle, + $sReconciliationKeys, + 1, + 0 + ); + } + + public function testImportFail_Enum(){ + $sFirstName = "firstname_UID"; + $sLastName = "lastname_UID"; + $sEmail = "email_UID@toto.fr"; + + $this->performImportTesting( + '"first_name","name", "email", "org_id->name", status', + sprintf('"%s", "%s", "%s", UID, toto', $sFirstName, $sLastName, $sEmail), + sprintf( + 'Issue: Unexpected attribute value(s);n/a;n/a;ORGID;"%s";"%s";"%s";\'toto\' is an invalid value. Unexpected value for attribute \'status\': Value not allowed [toto]', $sFirstName, $sLastName, $sEmail + ), + null, + 1, + 0 + ); + } + + public function testImportFail_Date(){ + $sFirstName = "firstname_UID"; + $sLastName = "lastname_UID"; + $sEmail = "email_UID@toto.fr"; + + $this->performImportTesting( + '"first_name","name", "email", "org_id->name", obsolescence_date', + sprintf('"%s", "%s", "%s", UID, toto', $sFirstName, $sLastName, $sEmail), + sprintf( + 'Issue: Internal error: Exception, Wrong format for date attribute obsolescence_date, expecting "Y-m-d" and got "toto";n/a;n/a;n/a;%s;%s;%s;toto', $sFirstName, $sLastName, $sEmail + ), + null, + 1, + 0 + ); + } + + private function performImportTesting($sCsvHeaders, $sCsvFirstLineValues, $sExpectedLastLineNeedle, $sReconciliationKeys=null, $iExpectedIssue=1, $iExpectedCreated=0) { + $sContent = <<sTmpFile, str_replace("UID", $this->sUid, $sContent)); + + $aParams = [ + 'class' => 'Person', + 'csvfile' => $this->sTmpFile, + 'charset' => 'UTF-8', + 'no_localize' => '1', + 'output' => 'details', + ]; + + if (null != $sReconciliationKeys){ + $aParams["reconciliationkeys"] = $sReconciliationKeys; + } + + $aRes = \utils::ExecITopScript('webservices/import.php', $aParams, $this->sLogin, $this->sPassword); + $aOutput = $aRes[1]; + $sOutput = implode("\n", $aOutput); + $sLastline = $aOutput[sizeof($aOutput) - 1]; + $iRes = $aRes[0]; + $this->assertEquals(0, $iRes, $sOutput); + $this->assertContains("#Issues: $iExpectedIssue", $sOutput, $sOutput); + $this->assertContains("#Warnings: 0", $sOutput, $sOutput); + $this->assertContains("#Created: $iExpectedCreated", $sOutput, $sOutput); + $this->assertContains("#Updated: 0", $sOutput, $sOutput); + var_dump($sLastline); + if ($iExpectedCreated === 1) { + $this->assertContains("created;Person", $sLastline, $sLastline); + } + + $iOrgId = $this->oOrg->GetKey(); + $sLastLineNeedle = $sExpectedLastLineNeedle; + foreach (["ORGID" => $iOrgId, "UID" => $this->sUid] as $sSearch => $sReplace){ + $sLastLineNeedle = str_replace($sSearch, $sReplace, $sLastLineNeedle); + } + $this->assertContains($sLastLineNeedle, $sLastline, $sLastline); + + $sPattern = "/Person;(\d+);/"; + if (preg_match($sPattern,$sLastline,$aMatches)){ + var_dump($aMatches); + $iObjId = $aMatches[1]; + $oObj = MetaModel::GetObject("Person", $iObjId); + $oObj->DBDelete(); + } + + //date + //ext key + } +} diff --git a/webservices/import.php b/webservices/import.php index a82707fa2..26235b7ea 100644 --- a/webservices/import.php +++ b/webservices/import.php @@ -232,7 +232,7 @@ if (utils::IsModeCLI()) { // Next steps: // specific arguments: 'csvfile' - // + // $sAuthUser = ReadMandatoryParam($oP, 'auth_user', 'raw_data'); $sAuthPwd = ReadMandatoryParam($oP, 'auth_pwd', 'raw_data'); $sCsvFile = ReadMandatoryParam($oP, 'csvfile', 'raw_data'); @@ -273,7 +273,7 @@ try // // Read parameters // - $sClass = ReadMandatoryParam($oP, 'class', 'raw_data'); // do not filter as a valid class, we want to produce the report "wrong class" ourselves + $sClass = ReadMandatoryParam($oP, 'class', 'raw_data'); // do not filter as a valid class, we want to produce the report "wrong class" ourselves $sSep = ReadParam($oP, 'separator', 'raw_data'); $sQualifier = ReadParam($oP, 'qualifier', 'raw_data'); $sCharSet = ReadParam($oP, 'charset', 'raw_data'); @@ -326,7 +326,7 @@ try { $sDateFormat = null; } - + if ($sCharSet == '') { $sCharSet = MetaModel::GetConfig()->Get('csv_file_default_charset'); @@ -444,7 +444,7 @@ try { $sUTF8Data = iconv($sCharSet, 'UTF-8//IGNORE//TRANSLIT', $sCSVData); } - $oCSVParser = new CSVParser($sUTF8Data, $sSep, $sQualifier); + $oCSVParser = new CSVParser($sUTF8Data, $sSep, $sQualifier); // Limitation: as the attribute list is in the first line, we can not match external key by a third-party attribute $aRawFieldList = $oCSVParser->ListFields(); @@ -466,7 +466,7 @@ try // Remove any trailing "star" character before the arrow (->) // A star character at the end can be used to indicate a mandatory field $sFieldName = $aMatches[1].'->'.$aMatches[2]; - } + } if (array_key_exists(strtolower($sFieldName), $aKnownColumnNames)) { $aColumns = $aKnownColumnNames[strtolower($sFieldName)]; @@ -488,7 +488,7 @@ try throw new BulkLoadException("Unknown column: '$sSafeName'. Possible columns: ".implode(', ', array_keys($aKnownColumnNames))); } } - // Note: at this stage the list of fields is supposed to be made of attcodes (and the symbol '->') + // Note: at this stage the list of fields is supposed to be made of attcodes (and the symbol '->') $aAttList = array(); $aExtKeys = array(); @@ -753,7 +753,7 @@ try break; case 'RowStatus_Issue': $iCountErrors++; - break; + break; } if ($bWritten) @@ -832,7 +832,7 @@ try $aDisplayConfig["$iCol"] = array("label"=>$sAttCode, "description"=>$sLabel); } } - + $aResultDisp = array(); // to be displayed foreach($aRes as $iRow => $aRowData) { @@ -859,14 +859,16 @@ try foreach($aRowData as $key => $value) { $sKey = (string) $key; - + if ($sKey == '__STATUS__') continue; + //__ERRORS__ used by tests only + if ($sKey == '__ERRORS__') continue; if ($sKey == 'finalclass') continue; if ($sKey == 'id') continue; - + if (is_object($value)) { - $aRowDisp["$sKey"] = $value->GetDisplayableValue().$value->GetDescription(); + $aRowDisp["$sKey"] = $value->GetDisplayableValueAndDescription(); } else { @@ -880,15 +882,15 @@ try } catch(BulkLoadException $e) { - $oP->add_comment($e->getMessage()); + $oP->add_comment($e->getMessage()); } catch(SecurityException $e) { - $oP->add_comment($e->getMessage()); + $oP->add_comment($e->getMessage()); } catch(Exception $e) { - $oP->add_comment((string)$e); + $oP->add_comment((string)$e); } $oP->output();