diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index ec4a04412..e50e37d35 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -895,6 +895,18 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay $aFields = explode(',', $aParams['fields']); } + $bFieldsAdvanced = false; + if (isset($aParams['fields_advanced'])) + { + $bFieldsAdvanced = (bool) $aParams['fields_advanced']; + } + + $bLocalize = true; + if (isset($aParams['localize_values'])) + { + $bLocalize = (bool) $aParams['localize_values']; + } + $aList = array(); $oAppContext = new ApplicationContext(); @@ -920,7 +932,29 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay // Standard list of attributes (no link sets) if ($oAttDef->IsScalar() && ($oAttDef->IsWritable() || $oAttDef->IsExternalField())) { - $aList[$sAlias][$sAttCode] = $oAttDef; + $sAttCodeEx = $oAttDef->IsExternalField() ? $oAttDef->GetKeyAttCode().'->'.$oAttDef->GetExtAttCode() : $sAttCode; + + if ($oAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) + { + if ($bFieldsAdvanced) + { + $aList[$sAlias][$sAttCodeEx] = $oAttDef; + + if ($oAttDef->IsExternalKey(EXTKEY_RELATIVE)) + { + $sRemoteClass = $oAttDef->GetTargetClass(); + foreach(MetaModel::GetReconcKeys($sRemoteClass) as $sRemoteAttCode) + { + $aList[$sAlias][$sAttCode.'->'.$sRemoteAttCode] = MetaModel::GetAttributeDef($sRemoteClass, $sRemoteAttCode); + } + } + } + } + else + { + // Any other attribute + $aList[$sAlias][$sAttCodeEx] = $oAttDef; + } } } else @@ -932,36 +966,18 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay } } } - $aHeader[] = 'id'; - foreach($aList[$sAlias] as $sAttCode => $oAttDef) + if ($bFieldsAdvanced) + { + $aHeader[] = 'id'; + } + foreach($aList[$sAlias] as $sAttCodeEx => $oAttDef) { $sStar = ''; - if ($oAttDef->IsExternalField()) + if (!$oAttDef->IsNullAllowed() && isset($aParams['showMandatoryFields'])) { - $sExtKeyLabel = MetaModel::GetLabel($sClassName, $oAttDef->GetKeyAttCode()); - $oExtKeyAttDef = MetaModel::GetAttributeDef($sClassName, $oAttDef->GetKeyAttCode()); - if (!$oExtKeyAttDef->IsNullAllowed() && isset($aParams['showMandatoryFields'])) - { - $sStar = '*'; - } - $sRemoteAttLabel = MetaModel::GetLabel($oAttDef->GetTargetClass(), $oAttDef->GetExtAttCode()); - $oTargetAttDef = MetaModel::GetAttributeDef($oAttDef->GetTargetClass(), $oAttDef->GetExtAttCode()); - $sSuffix = ''; - if ($oTargetAttDef->IsExternalKey()) - { - $sSuffix = '->id'; - } - - $aHeader[] = $sExtKeyLabel.'->'.$sRemoteAttLabel.$sSuffix.$sStar; - } - else - { - if (!$oAttDef->IsNullAllowed() && isset($aParams['showMandatoryFields'])) - { - $sStar = '*'; - } - $aHeader[] = MetaModel::GetLabel($sClassName, $sAttCode).$sStar; + $sStar = '*'; } + $aHeader[] = ($bLocalize ? MetaModel::GetLabel($sClassName, $sAttCodeEx) : $sAttCodeEx).$sStar; } } $sHtml = implode($sSeparator, $aHeader)."\n"; @@ -972,15 +988,7 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay foreach($aAuthorizedClasses as $sAlias => $sClassName) { $oObj = $aObjects[$sAlias]; - if (is_null($oObj)) - { - $aRow[] = ''; - } - else - { - $aRow[] = $oObj->GetKey(); - } - foreach($aList[$sAlias] as $sAttCode => $oAttDef) + if ($bFieldsAdvanced) { if (is_null($oObj)) { @@ -988,7 +996,19 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay } else { - $aRow[] = $oObj->GetAsCSV($sAttCode, $sSeparator, $sTextQualifier); + $aRow[] = $oObj->GetKey(); + } + } + foreach($aList[$sAlias] as $sAttCodeEx => $oAttDef) + { + if (is_null($oObj)) + { + $aRow[] = ''; + } + else + { + $value = $oObj->Get($sAttCodeEx); + $aRow[] = $oAttDef->GetAsCSV($value, $sSeparator, $sTextQualifier, $oObj, $bLocalize); } } } @@ -1015,6 +1035,18 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay $aFields = explode(',', $aParams['fields']); } + $bFieldsAdvanced = false; + if (isset($aParams['fields_advanced'])) + { + $bFieldsAdvanced = (bool) $aParams['fields_advanced']; + } + + $bLocalize = true; + if (isset($aParams['localize_values'])) + { + $bLocalize = (bool) $aParams['localize_values']; + } + $aList = array(); $oAppContext = new ApplicationContext(); @@ -1040,7 +1072,18 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay // Standard list of attributes (no link sets) if ($oAttDef->IsScalar() && ($oAttDef->IsWritable() || $oAttDef->IsExternalField())) { - $aList[$sAlias][$sAttCode] = $oAttDef; + $sAttCodeEx = $oAttDef->IsExternalField() ? $oAttDef->GetKeyAttCode().'->'.$oAttDef->GetExtAttCode() : $sAttCode; + + $aList[$sAlias][$sAttCodeEx] = $oAttDef; + + if ($bFieldsAdvanced && $oAttDef->IsExternalKey(EXTKEY_RELATIVE)) + { + $sRemoteClass = $oAttDef->GetTargetClass(); + foreach(MetaModel::GetReconcKeys($sRemoteClass) as $sRemoteAttCode) + { + $aList[$sAlias][$sAttCode.'->'.$sRemoteAttCode] = MetaModel::GetAttributeDef($sRemoteClass, $sRemoteAttCode); + } + } } } else @@ -1067,25 +1110,10 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay } } - foreach($aList[$sAlias] as $sAttCode => $oAttDef) + foreach($aList[$sAlias] as $sAttCodeEx => $oAttDef) { - if ($oAttDef->IsExternalField()) - { - $sExtKeyLabel = MetaModel::GetLabel($sClassName, $oAttDef->GetKeyAttCode()); - $oExtKeyAttDef = MetaModel::GetAttributeDef($sClassName, $oAttDef->GetKeyAttCode()); - $sRemoteAttLabel = MetaModel::GetLabel($oAttDef->GetTargetClass(), $oAttDef->GetExtAttCode()); - $oTargetAttDef = MetaModel::GetAttributeDef($oAttDef->GetTargetClass(), $oAttDef->GetExtAttCode()); - $sSuffix = ''; - if ($oTargetAttDef->IsExternalKey()) - { - $sSuffix = '->id'; - } - $sColLabel = $sExtKeyLabel.'->'.$sRemoteAttLabel.$sSuffix; - } - else - { - $sColLabel = MetaModel::GetLabel($sClassName, $sAttCode); - } + $sColLabel = $bLocalize ? MetaModel::GetLabel($sClassName, $sAttCodeEx) : $sAttCodeEx; + $oFinalAttDef = $oAttDef->GetFinalAttDef(); if (get_class($oFinalAttDef) == 'AttributeDateTime') { @@ -1111,8 +1139,7 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay foreach($aAuthorizedClasses as $sAlias => $sClassName) { $oObj = $aObjects[$sAlias]; - foreach($aList[$sAlias] as $sAttCode => $oAttDef) - + foreach($aList[$sAlias] as $sAttCodeEx => $oAttDef) { if (is_null($oObj)) { @@ -1123,13 +1150,22 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay $oFinalAttDef = $oAttDef->GetFinalAttDef(); if (get_class($oFinalAttDef) == 'AttributeDateTime') { - $iDate = AttributeDateTime::GetAsUnixSeconds($oObj->Get($sAttCode)); + $iDate = AttributeDateTime::GetAsUnixSeconds($oObj->Get($sAttCodeEx)); $aRow[] = ''.date('Y-m-d', $iDate).''; $aRow[] = ''.date('H:i:s', $iDate).''; } else { - $aRow[] = ''.(string) $oObj->Get($sAttCode).''; + $rawValue = $oObj->Get($sAttCodeEx); + if ($bLocalize) + { + $outputValue = htmlentities($oFinalAttDef->GetValueLabel($rawValue), ENT_QUOTES, 'UTF-8'); + } + else + { + $outputValue = htmlentities($rawValue, ENT_QUOTES, 'UTF-8'); + } + $aRow[] = ''.$outputValue.''; } } } @@ -1144,6 +1180,12 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay static function DisplaySetAsXML(WebPage $oPage, CMDBObjectSet $oSet, $aParams = array()) { + $bLocalize = true; + if (isset($aParams['localize_values'])) + { + $bLocalize = (bool) $aParams['localize_values']; + } + $oAppContext = new ApplicationContext(); $aClasses = $oSet->GetFilter()->GetSelectedClasses(); $aAuthorizedClasses = array(); @@ -1189,7 +1231,7 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay { if (!$oAttDef->IsLinkSet()) { - $sValue = $oObj->GetAsXML($sAttCode); + $sValue = $oObj->GetAsXML($sAttCode, $bLocalize); $oPage->add("<$sAttCode>$sValue\n"); } } diff --git a/application/csvpage.class.inc.php b/application/csvpage.class.inc.php index 0d657776e..05b47af02 100644 --- a/application/csvpage.class.inc.php +++ b/application/csvpage.class.inc.php @@ -31,8 +31,9 @@ class CSVPage extends WebPage function __construct($s_title) { parent::__construct($s_title); - $this->add_header("Content-type: text/html; charset=utf-8"); + $this->add_header("Content-type: text/plain; charset=utf-8"); $this->add_header("Cache-control: no-cache"); + //$this->add_header("Content-Transfer-Encoding: binary"); } public function output() diff --git a/application/datatable.class.inc.php b/application/datatable.class.inc.php index 44bd7e3c8..b9a20e9fe 100644 --- a/application/datatable.class.inc.php +++ b/application/datatable.class.inc.php @@ -73,6 +73,10 @@ class DataTable $this->oSet->SetOrderBy($oCustomSettings->GetSortOrder()); $bToolkitMenu = true; + if (isset($aExtraParams['toolkit_menu'])) + { + $bToolkitMenu = (bool) $aExtraParams['toolkit_menu']; + } if (UserRights::IsPortalUser()) { // Portal users have a limited access to data, for now they can only see what's configured for them @@ -341,6 +345,12 @@ EOF; protected function GetHTMLTableValues($aColumns, $sSelectMode, $iPageSize, $bViewLink, $aExtraParams) { + $bLocalize = true; + if (isset($aExtraParams['localize_values'])) + { + $bLocalize = (bool) $aExtraParams['localize_values']; + } + $aValues = array(); $this->oSet->Seek(0); $iMaxObjects = $iPageSize; @@ -384,7 +394,7 @@ EOF; } else { - $aRow[$sAttCode.'_'.$sAlias] = $aObjects[$sAlias]->GetAsHTML($sAttCode); + $aRow[$sAttCode.'_'.$sAlias] = $aObjects[$sAlias]->GetAsHTML($sAttCode, $bLocalize); } } } diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php index 964e93d82..5b05ff194 100644 --- a/application/displayblock.class.inc.php +++ b/application/displayblock.class.inc.php @@ -711,7 +711,7 @@ class DisplayBlock $oFilter->AddCondition($sStateAttrCode, $sStateValue, '='); $oSet = new DBObjectSet($oFilter); $aCounts[$sStateValue] = $oSet->Count(); - $aStateLabels[$sStateValue] = $oAttDef->GetValueLabel($sStateValue); + $aStateLabels[$sStateValue] = htmlentities($oAttDef->GetValueLabel($sStateValue), ENT_QUOTES, 'UTF-8'); if ($aCounts[$sStateValue] == 0) { $aCounts[$sStateValue] = '-'; @@ -733,8 +733,67 @@ class DisplayBlock break; case 'csv': + $bAdvancedMode = utils::ReadParam('advanced', false); + + $sCsvFile = strtolower($this->m_oFilter->GetClass()).'.csv'; + $sDownloadLink = utils::GetAbsoluteUrlAppRoot().'webservices/export.php?expression='.urlencode($this->m_oFilter->ToOQL()).'&format=csv&filename='.urlencode($sCsvFile); + $sLinkToToggle = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=search&'.$oAppContext->GetForLink().'&filter='.urlencode($this->m_oFilter->serialize()).'&format=csv'; + if ($bAdvancedMode) + { + $sDownloadLink .= '&fields_advanced=1'; + $sChecked = 'CHECKED'; + } + else + { + $sLinkToToggle = $sLinkToToggle.'&advanced=1'; + $sChecked = ''; + } + + $sCSVData = cmdbAbstractObject::GetSetAsCSV($this->m_oSet, array('fields_advanced' => $bAdvancedMode)); + $sCharset = MetaModel::GetConfig()->Get('csv_file_default_charset'); + if ($sCharset == 'UTF-8') + { + $bLostChars = false; + } + else + { + $sConverted = @iconv('UTF-8', $sCharset, $sCSVData); + $sRestored = @iconv($sCharset, 'UTF-8', $sConverted); + $bLostChars = ($sRestored != $sCSVData); + } + + if ($bLostChars) + { + $sCharsetNotice = "  "; + $sCharsetNotice .= ''; + $sCharsetNotice .= ""; + + $sTip = "

".htmlentities(Dict::S('UI:CSVExport:LostChars'), ENT_QUOTES, 'UTF-8')."

"; + $sTip .= "

".htmlentities(Dict::Format('UI:CSVExport:LostChars+', $sCharset), ENT_QUOTES, 'UTF-8')."

"; + $oPage->add_ready_script("$('#csv_charset_issue').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );"); + } + else + { + $sCharsetNotice = ''; + } + + $sHtml .= "
"; + $sHtml .= ''; + $sHtml .= ''; + $sHtml .= ''; + $sHtml .= ''; + $sHtml .= ''; + $sHtml .= '
'.Dict::Format('UI:Download-CSV', $sCsvFile).''.$sCharsetNotice.' '.Dict::S('UI:CSVExport:AdvancedMode').'
'; + if ($bAdvancedMode) + { + $sHtml .= "

"; + $sHtml .= htmlentities(Dict::S('UI:CSVExport:AdvancedMode+'), ENT_QUOTES, 'UTF-8'); + $sHtml .= "

"; + } + $sHtml .= "
"; + $sHtml .= "\n"; break; diff --git a/application/itopwebpage.class.inc.php b/application/itopwebpage.class.inc.php index 8b35e6aa0..623ca0b00 100644 --- a/application/itopwebpage.class.inc.php +++ b/application/itopwebpage.class.inc.php @@ -60,8 +60,6 @@ class iTopWebPage extends NiceWebPage $this->add_linked_script('../js/jquery.ba-bbq.min.js'); $this->add_linked_script("../js/jquery.treeview.js"); $this->add_linked_script("../js/jquery.autocomplete.js"); - $this->add_linked_script("../js/jquery.positionBy.js"); - $this->add_linked_script("../js/jquery.popupmenu.js"); $this->add_linked_script("../js/date.js"); $this->add_linked_script("../js/jquery.blockUI.js"); $this->add_linked_script("../js/utils.js"); @@ -77,8 +75,6 @@ class iTopWebPage extends NiceWebPage $this->add_linked_script('../js/g.pie.js'); $this->add_linked_script('../js/g.dot.js'); $this->add_linked_script('../js/charts.js'); - $this->add_linked_script('../js/field_sorter.js'); - $this->add_linked_script('../js/datatable.js'); $this->m_sInitScript = <<< EOF diff --git a/application/nicewebpage.class.inc.php b/application/nicewebpage.class.inc.php index 312e2bc9e..5976755ce 100644 --- a/application/nicewebpage.class.inc.php +++ b/application/nicewebpage.class.inc.php @@ -44,6 +44,10 @@ class NiceWebPage extends WebPage $this->add_linked_script("../js/jquery.tablesorter.min.js"); $this->add_linked_script("../js/jquery.tablesorter.pager.js"); $this->add_linked_script("../js/jquery.tablehover.js"); + $this->add_linked_script('../js/field_sorter.js'); + $this->add_linked_script('../js/datatable.js'); + $this->add_linked_script("../js/jquery.positionBy.js"); + $this->add_linked_script("../js/jquery.popupmenu.js"); $this->add_ready_script( <<< EOF //add new widget called TruncatedList to properly display truncated lists when they are sorted diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index fecac2f81..f7d2da4a6 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -235,12 +235,21 @@ abstract class AttributeDefinition } /** - * Get the label corresponding to the given value + * Get the label corresponding to the given value (in plain text) * To be overloaded for localized enums */ public function GetValueLabel($sValue) { - return $this->GetAsHTML($sValue); + return $sValue; + } + + /** + * Get the value from a given string (plain text, CSV import) + * Return null if no match could be found + */ + public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) + { + return $this->MakeRealValue($sProposedValue, null); } public function GetLabel_Obsolete() @@ -422,7 +431,7 @@ abstract class AttributeDefinition /** * Override to display the value in the GUI */ - public function GetAsHTML($sValue, $oHostObject = null) + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) { return Str::pure2html((string)$sValue); } @@ -430,7 +439,7 @@ abstract class AttributeDefinition /** * Override to export the value in XML */ - public function GetAsXML($sValue, $oHostObject = null) + public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true) { return Str::pure2xml((string)$sValue); } @@ -438,7 +447,7 @@ abstract class AttributeDefinition /** * Override to escape the value when read by DBObject::GetAsCSV() */ - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) { return (string)$sValue; } @@ -591,7 +600,7 @@ class AttributeLinkedSet extends AttributeDefinition public function GetBasicFilterLooseOperator() {return '';} public function GetBasicFilterSQLExpr($sOpCode, $value) {return '';} - public function GetAsHTML($sValue, $oHostObject = null) + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) { if (is_object($sValue) && ($sValue instanceof DBObjectSet)) { @@ -619,12 +628,12 @@ class AttributeLinkedSet extends AttributeDefinition return null; } - public function GetAsXML($sValue, $oHostObject = null) + public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true) { return "Sorry, no yet implemented"; } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) { $sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator'); $sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator'); @@ -684,8 +693,7 @@ class AttributeLinkedSet extends AttributeDefinition return $aColumns; } - // Specific to this kind of attribute : transform a string into a value - public function MakeValueFromString($sProposedValue, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) + public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) { if (is_null($sSepItem) || empty($sSepItem)) { @@ -1080,7 +1088,7 @@ class AttributeInteger extends AttributeDBField */ class AttributePercentage extends AttributeInteger { - public function GetAsHTML($sValue, $oHostObject = null) + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) { $iWidth = 5; // Total width of the percentage bar graph, in em... $iValue = (int)$sValue; @@ -1237,7 +1245,7 @@ class AttributeBoolean extends AttributeInteger return 0; } - public function GetAsXML($sValue, $oHostObject = null) + public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true) { return $sValue ? '1' : '0'; } @@ -1355,7 +1363,7 @@ class AttributeString extends AttributeDBField return $value; } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) { $sFrom = array("\r\n", $sTextQualifier); $sTo = array("\n", $sTextQualifier.$sTextQualifier); @@ -1403,7 +1411,7 @@ class AttributeClass extends AttributeString return $sDefault; } - public function GetAsHTML($sValue, $oHostObject = null) + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) { if (empty($sValue)) return ''; return MetaModel::GetName($sValue); @@ -1492,7 +1500,7 @@ class AttributeFinalClass extends AttributeString return $this->m_sValue; } - public function GetAsHTML($sValue, $oHostObject = null) + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) { if (empty($sValue)) return ''; return MetaModel::GetName($sValue); @@ -1550,7 +1558,7 @@ class AttributePassword extends AttributeString return array(); } - public function GetAsHTML($sValue, $oHostObject = null) + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) { if (strlen($sValue) == 0) { @@ -1708,9 +1716,9 @@ class AttributeText extends AttributeString return $sText; } - public function GetAsHTML($sValue, $oHostObject = null) + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) { - $sValue = parent::GetAsHTML($sValue); + $sValue = parent::GetAsHTML($sValue, $oHostObject, $bLocalize); $sValue = self::RenderWikiHtml($sValue); $aStyles = array(); if ($this->GetWidth() != '') @@ -1772,7 +1780,7 @@ class AttributeText extends AttributeString return $sValue; } - public function GetAsXML($value, $oHostObject = null) + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) { return Str::pure2xml($value); } @@ -1948,7 +1956,7 @@ class AttributeCaseLog extends AttributeLongText return $aColumns; } - public function GetAsHTML($value, $oHostObject = null) + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) { if ($value instanceOf ormCaseLog) { @@ -1975,11 +1983,11 @@ class AttributeCaseLog extends AttributeLongText return "
".$sContent.'
'; } - public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null) + public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) { if ($value instanceOf ormCaseLog) { - return parent::GetAsCSV($value->GetText(), $sSeparator, $sTextQualifier, $oHostObject); + return parent::GetAsCSV($value->GetText(), $sSeparator, $sTextQualifier, $oHostObject, $bLocalize); } else { @@ -1987,11 +1995,11 @@ class AttributeCaseLog extends AttributeLongText } } - public function GetAsXML($value, $oHostObject = null) + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) { if ($value instanceOf ormCaseLog) { - return parent::GetAsXML($value->GetText(), $oHostObject); + return parent::GetAsXML($value->GetText(), $oHostObject, $bLocalize); } else { @@ -2009,7 +2017,7 @@ class AttributeHTML extends AttributeLongText { public function GetEditClass() {return "HTML";} - public function GetAsHTML($sValue, $oHostObject = null) + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) { return $sValue; } @@ -2028,7 +2036,7 @@ class AttributeEmailAddress extends AttributeString return "^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$"; } - public function GetAsHTML($sValue, $oHostObject = null) + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) { if (empty($sValue)) return ''; return ''.parent::GetAsHTML($sValue).''; @@ -2092,7 +2100,7 @@ class AttributeTemplateHTML extends AttributeText { public function GetEditClass() {return "HTML";} - public function GetAsHTML($sValue, $oHostObject = null) + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) { return $sValue; } @@ -2226,14 +2234,51 @@ class AttributeEnum extends AttributeString return $sDescription; } - public function GetAsHTML($sValue, $oHostObject = null) + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) { - $sLabel = $this->GetValueLabel($sValue); - $sDescription = $this->GetValueDescription($sValue); - // later, we could imagine a detailed description in the title - return "".parent::GetAsHtml($sLabel).""; + if ($bLocalize) + { + $sLabel = $this->GetValueLabel($sValue); + $sDescription = $this->GetValueDescription($sValue); + // later, we could imagine a detailed description in the title + $sRes = "".parent::GetAsHtml($sLabel).""; + } + else + { + $sRes = parent::GetAsHtml($sValue, $oHostObject, $bLocalize); + } + return $sRes; } + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + if ($bLocalize) + { + $sFinalValue = $this->GetValueLabel($value); + } + else + { + $sFinalValue = $value; + } + $sRes = parent::GetAsXML($sFinalValue, $oHostObject, $bLocalize); + return $sRes; + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + { + if ($bLocalize) + { + $sFinalValue = $this->GetValueLabel($sValue); + } + else + { + $sFinalValue = $sValue; + } + $sRes = parent::GetAsCSV($sFinalValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize); + return $sRes; + } + + public function GetEditValue($sValue, $oHostObj = null) { return $this->GetValueLabel($sValue); @@ -2254,11 +2299,46 @@ class AttributeEnum extends AttributeString $aLocalizedValues = array(); foreach ($aRawValues as $sKey => $sValue) { - $aLocalizedValues[$sKey] = $this->GetValueLabel($sKey); + $aLocalizedValues[$sKey] = Str::pure2html($this->GetValueLabel($sKey)); } return $aLocalizedValues; } + /** + * An enum can be localized + */ + public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) + { + if ($bLocalizedValue) + { + // Lookup for the value matching the input + // + $sFoundValue = null; + $aRawValues = parent::GetAllowedValues(); + if (!is_null($aRawValues)) + { + foreach ($aRawValues as $sKey => $sValue) + { + $sRefValue = $this->GetValueLabel($sKey); + if ($sProposedValue == $sRefValue) + { + $sFoundValue = $sKey; + break; + } + } + } + if (is_null($sFoundValue)) + { + return null; + } + return $this->MakeRealValue($sFoundValue, null); + } + else + { + return parent::MakeValueFromString($sProposedValue, $bLocalizedValue, $sSepItem, $sSepAttribute, $sSepValue, $sAttributeQualifier); + } + } + /** * Processes the input value to align it with the values supported * by this type of attribute. In this case: turns empty strings into nulls @@ -2424,17 +2504,17 @@ class AttributeDateTime extends AttributeDBField return $value; } - public function GetAsHTML($value, $oHostObject = null) + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) { return Str::pure2html($value); } - public function GetAsXML($value, $oHostObject = null) + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) { return Str::pure2xml($value); } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) { $sFrom = array("\r\n", $sTextQualifier); $sTo = array("\n", $sTextQualifier.$sTextQualifier); @@ -2546,7 +2626,7 @@ class AttributeDuration extends AttributeInteger return $value; } - public function GetAsHTML($value, $oHostObject = null) + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) { return Str::pure2html(self::FormatDuration($value)); } @@ -2625,7 +2705,7 @@ class AttributeDate extends AttributeDateTime */ class AttributeDeadline extends AttributeDateTime { - public function GetAsHTML($value, $oHostObject = null) + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) { $sResult = self::FormatDeadline($value); return $sResult; @@ -3140,20 +3220,20 @@ class AttributeExternalField extends AttributeDefinition return $oExtAttDef->FromSQLToValue($aCols, $sPrefix); } - public function GetAsHTML($value, $oHostObject = null) + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) { $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetAsHTML($value); + return $oExtAttDef->GetAsHTML($value, null, $bLocalize); } - public function GetAsXML($value, $oHostObject = null) + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) { $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetAsXML($value); + return $oExtAttDef->GetAsXML($value, null, $bLocalize); } - public function GetAsCSV($value, $sSeparator = ',', $sTestQualifier = '"', $oHostObject = null) + public function GetAsCSV($value, $sSeparator = ',', $sTestQualifier = '"', $oHostObject = null, $bLocalize = true) { $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetAsCSV($value, $sSeparator, $sTestQualifier); + return $oExtAttDef->GetAsCSV($value, $sSeparator, $sTestQualifier, null, $bLocalize); } } @@ -3172,7 +3252,7 @@ class AttributeURL extends AttributeString public function GetEditClass() {return "String";} - public function GetAsHTML($sValue, $oHostObject = null) + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) { $sTarget = $this->Get("target"); if (empty($sTarget)) $sTarget = "_blank"; @@ -3322,7 +3402,7 @@ class AttributeBlob extends AttributeDefinition return 'true'; } - public function GetAsHTML($value, $oHostObject = null) + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) { if (is_object($value)) { @@ -3330,12 +3410,12 @@ class AttributeBlob extends AttributeDefinition } } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) { return ''; // Not exportable in CSV ! } - public function GetAsXML($value, $oHostObject = null) + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) { return ''; // Not exportable in XML, or as CDATA + some subtags ?? } @@ -3561,7 +3641,7 @@ class AttributeStopWatch extends AttributeDefinition return 'true'; } - public function GetAsHTML($value, $oHostObject = null) + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) { if (is_object($value)) { @@ -3569,12 +3649,12 @@ class AttributeStopWatch extends AttributeDefinition } } - public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null) + public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) { return $value->GetTimeSpent(); } - public function GetAsXML($value, $oHostObject = null) + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) { return $value->GetTimeSpent(); } @@ -3891,21 +3971,21 @@ class AttributeSubItem extends AttributeDefinition return $res; } - public function GetAsHTML($value, $oHostObject = null) + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) { $oParent = $this->GetTargetAttDef(); $res = $oParent->GetSubItemAsHTML($this->Get('item_code'), $value); return $res; } - public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null) + public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) { $oParent = $this->GetTargetAttDef(); $res = $oParent->GetSubItemAsCSV($this->Get('item_code'), $value, $sSeparator = ',', $sTextQualifier = '"'); return $res; } - public function GetAsXML($value, $oHostObject = null) + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) { $oParent = $this->GetTargetAttDef(); $res = $oParent->GetSubItemAsXML($this->Get('item_code'), $value); @@ -4057,7 +4137,7 @@ class AttributeOneWayPassword extends AttributeDefinition return 'true'; } - public function GetAsHTML($value, $oHostObject = null) + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) { if (is_object($value)) { @@ -4065,12 +4145,12 @@ class AttributeOneWayPassword extends AttributeDefinition } } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) { return ''; // Not exportable in CSV } - public function GetAsXML($value, $oHostObject = null) + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) { return ''; // Not exportable in XML } @@ -4132,7 +4212,7 @@ class AttributeTable extends AttributeDBField return $aValues; } - public function GetAsHTML($value, $oHostObject = null) + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) { if (!is_array($value)) { @@ -4160,13 +4240,13 @@ class AttributeTable extends AttributeDBField return $sRes; } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) { // Not implemented return ''; } - public function GetAsXML($value, $oHostObject = null) + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) { if (count($value) == 0) { @@ -4203,7 +4283,7 @@ class AttributePropertySet extends AttributeTable return $proposedValue; } - public function GetAsHTML($value, $oHostObject = null) + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) { if (!is_array($value)) { @@ -4232,7 +4312,7 @@ class AttributePropertySet extends AttributeTable return $sRes; } - public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null) + public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) { if (count($value) == 0) { @@ -4258,7 +4338,7 @@ class AttributePropertySet extends AttributeTable return $sTextQualifier.$sEscaped.$sTextQualifier; } - public function GetAsXML($value, $oHostObject = null) + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) { if (count($value) == 0) { @@ -4455,7 +4535,7 @@ class AttributeFriendlyName extends AttributeComputedFieldVoid return $this->m_sValue; } - public function GetAsHTML($sValue, $oHostObject = null) + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) { return Str::pure2html((string)$sValue); } diff --git a/core/bulkchange.class.inc.php b/core/bulkchange.class.inc.php index 47e31a215..7daf5e4d0 100644 --- a/core/bulkchange.class.inc.php +++ b/core/bulkchange.class.inc.php @@ -24,6 +24,10 @@ */ +// The BOM is added at the head of exported UTF-8 CSV data, and removed (if present) from input UTF-8 data. +// This helps MS-Excel (Version > 2007, Windows only) in changing its interpretation of a CSV file (by default Excel reads data as ISO-8859-1 -not 100% sure!) +define('UTF8_BOM', chr(239).chr(187).chr(191)); // 0xEF, 0xBB, 0xBF + /** * BulkChange * Interpret a given data set and update the DB accordingly (fake mode avail.) @@ -84,15 +88,16 @@ class CellStatus_Modify extends CellChangeSpec { protected $m_previousValue; - public function __construct($proposedValue, $previousValue) + public function __construct($proposedValue, $previousValue = null) { - $this->m_previousValue = $previousValue; + // Unused (could be costly to know -see the case of reconciliation on ext keys) + //$this->m_previousValue = $previousValue; parent::__construct($proposedValue); } public function GetDescription() { - return 'Modified'; + return Dict::S('UI:CSVReport-Value-Modified'); } //public function GetPreviousValue() @@ -115,9 +120,9 @@ class CellStatus_Issue extends CellStatus_Modify { if (is_null($this->m_proposedValue)) { - return 'Could not be changed - reason: '.$this->m_sReason; + return Dict::Format('UI:CSVReport-Value-SetIssue', $this->m_sReason); } - return 'Could not be changed to '.$this->m_proposedValue.' - reason: '.$this->m_sReason; + return Dict::Format('UI:CSVReport-Value-ChangeIssue', $this->m_proposedValue, $this->m_sReason); } } @@ -130,7 +135,7 @@ class CellStatus_SearchIssue extends CellStatus_Issue public function GetDescription() { - return 'No match'; + return Dict::S('UI:CSVReport-Value-NoMatch'); } } @@ -143,7 +148,7 @@ class CellStatus_NullIssue extends CellStatus_Issue public function GetDescription() { - return 'Missing mandatory value'; + return Dict::S('UI:CSVReport-Value-Missing'); } } @@ -162,7 +167,7 @@ class CellStatus_Ambiguous extends CellStatus_Issue public function GetDescription() { $sCount = $this->m_iCount; - return "Ambiguous: found $sCount objects"; + return Dict::Format('UI:CSVReport-Value-Ambiguous', $sCount); } } @@ -186,7 +191,7 @@ class RowStatus_NoChange extends RowStatus { public function GetDescription() { - return "unchanged"; + return Dict::S('UI:CSVReport-Row-Unchanged'); } } @@ -194,7 +199,7 @@ class RowStatus_NewObj extends RowStatus { public function GetDescription() { - return "created"; + return Dict::S('UI:CSVReport-Row-Created'); } } @@ -209,7 +214,7 @@ class RowStatus_Modify extends RowStatus public function GetDescription() { - return "updated ".$this->m_iChanged." cols"; + return Dict::Format('UI:CSVReport-Row-Updated', $this->m_iChanged); } } @@ -217,7 +222,7 @@ class RowStatus_Disappeared extends RowStatus_Modify { public function GetDescription() { - return "disappeared, changed ".$this->m_iChanged." cols"; + return Dict::Format('UI:CSVReport-Row-Disappeared', $this->m_iChanged); } } @@ -232,7 +237,7 @@ class RowStatus_Issue extends RowStatus public function GetDescription() { - return 'Issue: '.$this->m_sReason; + return Dict::Format('UI:CSVReport-Row-Issue', $this->m_sReason); } } @@ -253,8 +258,9 @@ class BulkChange 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 utils::StringToTime() + protected $m_bLocalizedValues; // Values in the data set are localized (see AttributeEnum) - public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null, $sDateFormat = null) + public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null, $sDateFormat = null, $bLocalize = false) { $this->m_sClass = $sClass; $this->m_aData = $aData; @@ -264,6 +270,7 @@ class BulkChange $this->m_sSynchroScope = $sSynchroScope; $this->m_aOnDisappear = $aOnDisappear; $this->m_sDateFormat = $sDateFormat; + $this->m_bLocalizedValues = $bLocalize; } protected $m_bReportHtml = false; @@ -331,6 +338,7 @@ class BulkChange { foreach ($aKeyConfig as $sForeignAttCode => $iCol) { + // Default reporting $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]); } if ($oExtKey->IsNullAllowed()) @@ -340,8 +348,8 @@ class BulkChange } else { - $aErrors[$sAttCode] = "Null not allowed"; - $aResults[$sAttCode]= new CellStatus_Issue(null, $oTargetObj->Get($sAttCode), 'Null not allowed'); + $aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-Null'); + $aResults[$sAttCode]= new CellStatus_Issue(null, $oTargetObj->Get($sAttCode), Dict::S('UI:CSVReport-Value-Issue-Null')); } } else @@ -357,7 +365,7 @@ class BulkChange switch($oExtObjects->Count()) { case 0: - $aErrors[$sAttCode] = "Object not found"; + $aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound'); $aResults[$sAttCode]= new CellStatus_SearchIssue(); break; case 1: @@ -366,7 +374,7 @@ class BulkChange $oTargetObj->Set($sAttCode, $oForeignObj->GetKey()); break; default: - $aErrors[$sAttCode] = "Found ".$oExtObjects->Count()." matches"; + $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $oExtObjects->Count()); $aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $oExtObjects->Count(), $oReconFilter->ToOql()); } } @@ -384,6 +392,11 @@ class BulkChange else { $aResults[$sAttCode]= new CellStatus_Modify($iForeignObj, $oTargetObj->GetOriginal($sAttCode)); + foreach ($aKeyConfig as $sForeignAttCode => $iCol) + { + // Report the change on reconciliation values as well + $aResults[$iCol] = new CellStatus_Modify($aRowData[$iCol]); + } } } else @@ -405,31 +418,39 @@ class BulkChange $iFlags = $oTargetObj->GetAttributeFlags($sAttCode, $aReasons); if ( (($iFlags & OPT_ATT_READONLY) == OPT_ATT_READONLY) && ( $oTargetObj->Get($sAttCode) != $aRowData[$iCol]) ) { - $aErrors[$sAttCode] = "the attribute '$sAttCode' is read-only and cannot be modified (current value: ".$oTargetObj->Get($sAttCode).", proposed value: {$aRowData[$iCol]})."; + $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-Readonly', $sAttCode, $oTargetObj->Get($sAttCode), $aRowData[$iCol]); } else if ($oAttDef->IsLinkSet() && $oAttDef->IsIndirect()) { try { - $oSet = $oAttDef->MakeValueFromString($aRowData[$iCol]); + $oSet = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); $oTargetObj->Set($sAttCode, $oSet); } catch(CoreException $e) { - $aErrors[$sAttCode] = "Failed to process input: ".$e->getMessage(); + $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-Format', $e->getMessage()); } } else { - $res = $oTargetObj->CheckValue($sAttCode, $aRowData[$iCol]); - if ($res === true) + $value = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); + if (is_null($value) && (strlen($aRowData[$iCol]) > 0)) { - $oTargetObj->Set($sAttCode, $aRowData[$iCol]); + $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode); } else { - // $res is a string with the error description - $aErrors[$sAttCode] = "Unexpected value for attribute '$sAttCode': $res"; + $res = $oTargetObj->CheckValue($sAttCode, $value); + if ($res === true) + { + $oTargetObj->Set($sAttCode, $value); + } + else + { + // $res is a string with the error description + $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-Unknown', $sAttCode, $res); + } } } } @@ -447,17 +468,19 @@ class BulkChange { if ($this->m_bReportHtml) { - $sCurValue = $oTargetObj->GetAsHTML($sAttCode); - $sOrigValue = $oTargetObj->GetOriginalAsHTML($sAttCode); + $sCurValue = $oTargetObj->GetAsHTML($sAttCode, $this->m_bLocalizedValues); + $sOrigValue = $oTargetObj->GetOriginalAsHTML($sAttCode, $this->m_bLocalizedValues); + $sInput = htmlentities($aRowData[$iCol], ENT_QUOTES, 'UTF-8'); } else { - $sCurValue = $oTargetObj->GetAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter); - $sOrigValue = $oTargetObj->GetOriginalAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter); + $sCurValue = $oTargetObj->GetAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter, $this->m_bLocalizedValues); + $sOrigValue = $oTargetObj->GetOriginalAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter, $this->m_bLocalizedValues); + $sInput = $aRowData[$iCol]; } if (isset($aErrors[$sAttCode])) { - $aResults[$iCol]= new CellStatus_Issue($sCurValue, $sOrigValue, $aErrors[$sAttCode]); + $aResults[$iCol]= new CellStatus_Issue($aRowData[$iCol], $sOrigValue, $aErrors[$sAttCode]); } elseif (array_key_exists($sAttCode, $aChangedFields)) { @@ -484,7 +507,7 @@ class BulkChange if ($res !== true) { // $res contains the error description - $aErrors["GLOBAL"] = "Attributes not consistent with each others: $res"; + $aErrors["GLOBAL"] = Dict::Format('UI:CSVReport-Row-Issue-Inconsistent', $res); } return $aResults; } @@ -548,7 +571,7 @@ class BulkChange if ($res !== true) { // $res contains the error description - $aErrors["GLOBAL"] = "Attributes not consistent with each others: $res"; + $aErrors["GLOBAL"] = Dict::Format('UI:CSVReport-Row-Issue-Inconsistent', $res); } return $aResults; } @@ -562,7 +585,7 @@ class BulkChange if (count($aErrors) > 0) { $sErrors = implode(', ', $aErrors); - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)"); + $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute')); return $oTargetObj; } @@ -581,7 +604,7 @@ class BulkChange if (count($aMissingKeys) > 0) { $sMissingKeys = implode(', ', $aMissingKeys); - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Could not be created, due to missing external key(s): $sMissingKeys"); + $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::Format('UI:CSVReport-Row-Issue-MissingExtKey', $sMissingKeys)); return $oTargetObj; } @@ -615,7 +638,7 @@ class BulkChange if (count($aErrors) > 0) { $sErrors = implode(', ', $aErrors); - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)"); + $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute')); return; } @@ -656,7 +679,7 @@ class BulkChange if (count($aErrors) > 0) { $sErrors = implode(', ', $aErrors); - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)"); + $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute')); return; } @@ -732,8 +755,8 @@ class BulkChange else { // Leave the cell unchanged - $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("wrong date format"); - $aResult[$iRow][$sAttCode] = new CellStatus_Issue(null, $this->m_aData[$iRow][$iCol], 'Wrong date format'); + $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat')); + $aResult[$iRow][$sAttCode] = new CellStatus_Issue(null, $this->m_aData[$iRow][$iCol], Dict::S('UI:CSVReport-Row-Issue-DateFormat')); } } } @@ -753,91 +776,98 @@ class BulkChange // An issue at the earlier steps - skip the rest continue; } - $oReconciliationFilter = new CMDBSearchFilter($this->m_sClass); - $bSkipQuery = false; - foreach($this->m_aReconcilKeys as $sAttCode) + try { - $valuecondition = null; - if (array_key_exists($sAttCode, $this->m_aExtKeys)) + $oReconciliationFilter = new CMDBSearchFilter($this->m_sClass); + $bSkipQuery = false; + foreach($this->m_aReconcilKeys as $sAttCode) { - if ($this->IsNullExternalKeySpec($aRowData, $sAttCode)) + $valuecondition = null; + if (array_key_exists($sAttCode, $this->m_aExtKeys)) { - $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - if ($oExtKey->IsNullAllowed()) + if ($this->IsNullExternalKeySpec($aRowData, $sAttCode)) { - $valuecondition = $oExtKey->GetNullValue(); - $aResult[$iRow][$sAttCode] = new CellStatus_Void($oExtKey->GetNullValue()); + $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + if ($oExtKey->IsNullAllowed()) + { + $valuecondition = $oExtKey->GetNullValue(); + $aResult[$iRow][$sAttCode] = new CellStatus_Void($oExtKey->GetNullValue()); + } + else + { + $aResult[$iRow][$sAttCode] = new CellStatus_NullIssue(); + } } else { - $aResult[$iRow][$sAttCode] = new CellStatus_NullIssue(); - } + // The value has to be found or verified + list($sQuery, $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(); + } + else + { + $aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $sQuery); + } + } } else { - // The value has to be found or verified - list($sQuery, $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(); - } - else - { - $aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $sQuery); - } - } - } - else - { - // The value is given in the data row - $iCol = $this->m_aAttList[$sAttCode]; - $valuecondition = $aRowData[$iCol]; - } - if (is_null($valuecondition)) - { - $bSkipQuery = true; - } - else - { - $oReconciliationFilter->AddCondition($sAttCode, $valuecondition, '='); - } - } - if ($bSkipQuery) - { - $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("failed to reconcile"); - } - else - { - $oReconciliationSet = new CMDBObjectSet($oReconciliationFilter); - switch($oReconciliationSet->Count()) - { - case 0: - $oTargetObj = $this->CreateObject($aResult, $iRow, $aRowData, $oChange); - // $aResult[$iRow]["__STATUS__"]=> set in CreateObject - $aVisited[] = $oTargetObj->GetKey(); - break; - case 1: - $oTargetObj = $oReconciliationSet->Fetch(); - $this->UpdateObject($aResult, $iRow, $oTargetObj, $aRowData, $oChange); - // $aResult[$iRow]["__STATUS__"]=> set in UpdateObject - if (!is_null($this->m_sSynchroScope)) - { - $aVisited[] = $oTargetObj->GetKey(); + // The value is given in the data row + $iCol = $this->m_aAttList[$sAttCode]; + $valuecondition = $aRowData[$iCol]; + } + if (is_null($valuecondition)) + { + $bSkipQuery = true; + } + else + { + $oReconciliationFilter->AddCondition($sAttCode, $valuecondition, '='); } - break; - default: - // Found several matches, ambiguous - $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("ambiguous reconciliation"); - $aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->ToOql()); - $aResult[$iRow]["finalclass"]= 'n/a'; } + if ($bSkipQuery) + { + $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Reconciliation')); + } + else + { + $oReconciliationSet = new CMDBObjectSet($oReconciliationFilter); + switch($oReconciliationSet->Count()) + { + case 0: + $oTargetObj = $this->CreateObject($aResult, $iRow, $aRowData, $oChange); + // $aResult[$iRow]["__STATUS__"]=> set in CreateObject + $aVisited[] = $oTargetObj->GetKey(); + break; + case 1: + $oTargetObj = $oReconciliationSet->Fetch(); + $this->UpdateObject($aResult, $iRow, $oTargetObj, $aRowData, $oChange); + // $aResult[$iRow]["__STATUS__"]=> set in UpdateObject + if (!is_null($this->m_sSynchroScope)) + { + $aVisited[] = $oTargetObj->GetKey(); + } + break; + 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]["finalclass"]= 'n/a'; + } + } + } + catch (Exception $e) + { + $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::Format('UI:CSVReport-Row-Issue-Internal', get_class($e), $e->getMessage())); } } @@ -1166,49 +1196,6 @@ EOF } $oPage->table($aConfig, $aDetails); } - - /** - * Get the user friendly name for an 'extended' attribute code i.e 'name', becomes 'Name' and 'org_id->name' becomes 'Organization->Name' - * @param string $sClassName The name of the class - * @param string $sAttCodeEx Either an attribute code or ext_key_name->att_code - * @return string A user friendly format of the string: AttributeName or AttributeName->ExtAttributeName - */ - public static function GetFriendlyAttCodeName($sClassName, $sAttCodeEx) - { - $sFriendlyName = ''; - if (preg_match('/(.+)->(.+)/', $sAttCodeEx, $aMatches) > 0) - { - $sAttribute = $aMatches[1]; - $sField = $aMatches[2]; - $oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttribute); - if ($oAttDef->IsExternalKey()) - { - $sTargetClass = $oAttDef->GetTargetClass(); - $oTargetAttDef = MetaModel::GetAttributeDef($sTargetClass, $sField); - $sFriendlyName = $oAttDef->GetLabel().'->'.$oTargetAttDef->GetLabel(); - } - else - { - // hum, hum... should never happen, we'd better raise an exception - throw(new Exception(Dict::Format('UI:CSVImport:ErrorExtendedAttCode', $sAttCodeEx, $sAttribute, $sClassName))); - } - - } - else - { - if ($sAttCodeEx == 'id') - { - $sFriendlyName = Dict::S('UI:CSVImport:idField'); - } - else - { - $oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCodeEx); - $sFriendlyName = $oAttDef->GetLabel(); - } - } - return $sFriendlyName; - } - } diff --git a/core/config.class.inc.php b/core/config.class.inc.php index 6c9a4677c..5f44a0a58 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -537,6 +537,16 @@ class Config 'source_of_value' => '', 'show_in_conf_sample' => true, ), + 'csv_file_default_charset' => array( + 'type' => 'string', + 'description' => 'Character set used by default for downloading and uploading data as a CSV file. Warning: it is case sensitive (uppercase is preferable).', + // examples... not used + 'default' => 'ISO-8859-1', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + ); public function IsProperty($sPropCode) diff --git a/core/dbobject.class.php b/core/dbobject.class.php index c8c228ac3..408b3083c 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -548,7 +548,7 @@ abstract class DBObject $this->ComputeValues(); } - public function GetAsHTML($sAttCode) + public function GetAsHTML($sAttCode, $bLocalize = true) { $sClass = get_class($this); $oAtt = MetaModel::GetAttributeDef($sClass, $sAttCode); @@ -571,7 +571,7 @@ abstract class DBObject } // That's a standard attribute (might be an ext field or a direct field, etc.) - return $oAtt->GetAsHTML($this->Get($sAttCode), $this); + return $oAtt->GetAsHTML($this->Get($sAttCode), $this, $bLocalize); } public function GetEditValue($sAttCode) @@ -609,34 +609,34 @@ abstract class DBObject return $sEditValue; } - public function GetAsXML($sAttCode) + public function GetAsXML($sAttCode, $bLocalize = true) { $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - return $oAtt->GetAsXML($this->Get($sAttCode), $this); + return $oAtt->GetAsXML($this->Get($sAttCode), $this, $bLocalize); } - public function GetAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"') + public function GetAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true) { $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - return $oAtt->GetAsCSV($this->Get($sAttCode), $sSeparator, $sTextQualifier, $this); + return $oAtt->GetAsCSV($this->Get($sAttCode), $sSeparator, $sTextQualifier, $this, $bLocalize); } - public function GetOriginalAsHTML($sAttCode) + public function GetOriginalAsHTML($sAttCode, $bLocalize = true) { $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - return $oAtt->GetAsHTML($this->GetOriginal($sAttCode), $this); + return $oAtt->GetAsHTML($this->GetOriginal($sAttCode), $this, $bLocalize); } - public function GetOriginalAsXML($sAttCode) + public function GetOriginalAsXML($sAttCode, $bLocalize = true) { $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - return $oAtt->GetAsXML($this->GetOriginal($sAttCode), $this); + return $oAtt->GetAsXML($this->GetOriginal($sAttCode), $this, $bLocalize); } - public function GetOriginalAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"') + public function GetOriginalAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true) { $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - return $oAtt->GetAsCSV($this->GetOriginal($sAttCode), $sSeparator, $sTextQualifier, $this); + return $oAtt->GetAsCSV($this->GetOriginal($sAttCode), $sSeparator, $sTextQualifier, $this, $bLocalize); } public static function MakeHyperLink($sObjClass, $sObjKey, $sLabel = '', $sUrlMakerClass = null, $bWithNavigationContext = true) diff --git a/core/metamodel.class.php b/core/metamodel.class.php index bc7d7c285..9a0aa51b1 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -907,11 +907,45 @@ abstract class MetaModel } - public static function GetLabel($sClass, $sAttCode) + /** + * Get the attribute label + * @param string sClass Persistent class + * @param string sAttCodeEx Extended attribute code: attcode[->attcode] + * @return string A user friendly format of the string: AttributeName or AttributeName->ExtAttributeName + */ + public static function GetLabel($sClass, $sAttCodeEx) { - $oAttDef = self::GetAttributeDef($sClass, $sAttCode); - if ($oAttDef) return $oAttDef->GetLabel(); - return ""; + $sLabel = ''; + if (preg_match('/(.+)->(.+)/', $sAttCodeEx, $aMatches) > 0) + { + $sAttribute = $aMatches[1]; + $sField = $aMatches[2]; + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttribute); + if ($oAttDef->IsExternalKey()) + { + $sTargetClass = $oAttDef->GetTargetClass(); + $oTargetAttDef = MetaModel::GetAttributeDef($sTargetClass, $sField); + $sLabel = $oAttDef->GetLabel().'->'.$oTargetAttDef->GetLabel(); + } + else + { + // Let's return something displayable... but this should never happen! + $sLabel = $oAttDef->GetLabel().'->'.$aMatches[2]; + } + } + else + { + if ($sAttCodeEx == 'id') + { + $sLabel = Dict::S('UI:CSVImport:idField'); + } + else + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCodeEx); + $sLabel = $oAttDef->GetLabel(); + } + } + return $sLabel; } public static function GetDescription($sClass, $sAttCode) diff --git a/dictionaries/dictionary.itop.ui.php b/dictionaries/dictionary.itop.ui.php index 2221f7cb2..2201e0b5b 100644 --- a/dictionaries/dictionary.itop.ui.php +++ b/dictionaries/dictionary.itop.ui.php @@ -550,7 +550,53 @@ Dict::Add('EN US', 'English', 'English', array( 'UI:UniversalSearchTitle' => 'iTop - Universal Search', 'UI:UniversalSearch:Error' => 'Error: %1$s', '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-Missing' => 'Missing mandatory value', + 'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects', + 'UI:CSVReport-Row-Unchanged' => 'unchanged', + 'UI:CSVReport-Row-Created' => 'created', + 'UI:CSVReport-Row-Updated' => 'updated %1$d cols', + 'UI:CSVReport-Row-Disappeared' => 'disappeared, changed %1$d cols', + 'UI:CSVReport-Row-Issue' => 'Issue: %1$s', + 'UI:CSVReport-Value-Issue-Null' => 'Null not allowed', + 'UI:CSVReport-Value-Issue-NotFound' => 'Object not found', + 'UI:CSVReport-Value-Issue-FoundMany' => 'Found %1$d matches', + '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-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-Reconciliation' => 'failed to reconcile', + 'UI:CSVReport-Row-Issue-Ambiguous' => 'ambiguous reconciliation', + 'UI:CSVReport-Row-Issue-Internal' => 'Internal error: %1$s, %2$s', + + 'UI:CSVReport-Icon-Unchanged' => 'Unchanged', + 'UI:CSVReport-Icon-Modified' => 'Modified', + 'UI:CSVReport-Icon-Missing' => 'Missing', + 'UI:CSVReport-Object-MissingToUpdate' => 'Missing object: will be updated', + 'UI:CSVReport-Object-MissingUpdated' => 'Missing object: updated', + 'UI:CSVReport-Icon-Created' => 'Created', + 'UI:CSVReport-Object-ToCreate' => 'Object will be created', + 'UI:CSVReport-Object-Created' => 'Object created', + 'UI:CSVReport-Icon-Error' => 'Error', + 'UI:CSVReport-Object-Error' => 'ERROR: %1$s', + 'UI:CSVReport-Object-Ambiguous' => 'AMBIGUOUS: %1$s', + 'UI:CSVReport-Stats-Errors' => '%1$.0f %% of the loaded objects have errors and will be ignored.', + 'UI:CSVReport-Stats-Created' => '%1$.0f %% of the loaded objects will be created.', + 'UI:CSVReport-Stats-Modified' => '%1$.0f %% of the loaded objects will be modified.', + + 'UI:CSVExport:AdvancedMode' => 'Advanced mode', + 'UI:CSVExport:AdvancedMode+' => 'In advanced mode, several columns are added to the export: the id of the object, the id of external keys and their reconciliation attributes.', + 'UI:CSVExport:LostChars' => 'Encoding issue', + 'UI:CSVExport:LostChars+' => 'The downloaded file will be encoded into %1$s. iTop has detected some characters that are not compatible with this format. Those characters will either be replaced by a substitute (e.g. accentuated chars losing the accent), or they will be discarded. You can copy/paste the data from your web browser. Alternatively, you can contact your administrator to change the encoding (See parameter \'csv_file_default_charset\').', + 'UI:Audit:Title' => 'iTop - CMDB Audit', 'UI:Audit:InteractiveAudit' => 'Interactive Audit', 'UI:Audit:HeaderAuditRule' => 'Audit Rule', @@ -870,6 +916,7 @@ When associated with a trigger, each action is given an "order" number, specifyi 'UI:OpenDocumentInNewWindow_' => 'Open this document in a new window: %1$s', 'UI:DownloadDocument_' => 'Download this document: %1$s', 'UI:Document:NoPreview' => 'No preview is available for this type of document', + 'UI:Download-CSV' => 'Download %1$s', 'UI:DeadlineMissedBy_duration' => 'Missed by %1$s', 'UI:Deadline_LessThan1Min' => '< 1 min', diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php index 0ee6b2230..ecfbed56b 100644 --- a/dictionaries/fr.dictionary.itop.ui.php +++ b/dictionaries/fr.dictionary.itop.ui.php @@ -423,6 +423,53 @@ Dict::Add('FR FR', 'French', 'Français', array( 'UI:CSVImport:AlertIncompleteMapping' => 'Veuillez choisir le correspondance de chacun des champs.', 'UI:CSVImport:AlertNoSearchCriteria' => 'Veuillez choisir au moins une clef de recherche.', 'UI:CSVImport:Encoding' => 'Encodage des caractères', + + '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-Missing' => 'Absence de valeur obligatoire', + 'UI:CSVReport-Value-Ambiguous' => 'Ambigüité: %1$d objets trouvés', + 'UI:CSVReport-Row-Unchanged' => 'inchangé', + 'UI:CSVReport-Row-Created' => 'créé', + 'UI:CSVReport-Row-Updated' => '%1$d colonnes modifiées', + 'UI:CSVReport-Row-Disappeared' => 'disparu, %1$d colonnes modifiées', + 'UI:CSVReport-Row-Issue' => 'Erreur: %1$s', + 'UI:CSVReport-Value-Issue-Null' => 'Valeur obligatoire', + 'UI:CSVReport-Value-Issue-NotFound' => 'Objet non trouvé', + 'UI:CSVReport-Value-Issue-FoundMany' => 'Plusieurs objets trouvés (%1$d)', + 'UI:CSVReport-Value-Issue-Readonly' => 'L\'attribut \'%1$s\' est en lecture seule (valeur courante: %2$s, valeur proposée: %3$s)', + 'UI:CSVReport-Value-Issue-Format' => 'Echec de traitement de la valeur: %1$s', + 'UI:CSVReport-Value-Issue-NoMatch' => 'Valeur incorrecte pour \'%1$s\': pas de correspondance, veuillez vérifier la syntaxe', + 'UI:CSVReport-Value-Issue-Unknown' => 'Valeur incorrecte pour \'%1$s\': %2$s', + 'UI:CSVReport-Row-Issue-Inconsistent' => 'Incohérence entre attributs: %1$s', + 'UI:CSVReport-Row-Issue-Attribute' => 'Des attributs ont des valeurs incorrectes', + 'UI:CSVReport-Row-Issue-MissingExtKey' => 'Ne peut pas être créé car il manque des clés externes : %1$s', + 'UI:CSVReport-Row-Issue-DateFormat' => 'Format de date incorrect', + 'UI:CSVReport-Row-Issue-Reconciliation' => 'Echec de réconciliation', + 'UI:CSVReport-Row-Issue-Ambiguous' => 'Réconciliation ambigüe', + 'UI:CSVReport-Row-Issue-Internal' => 'Erreur interne: %1$s, %2$s', + + 'UI:CSVReport-Icon-Unchanged' => 'Non modifié', + 'UI:CSVReport-Icon-Modified' => 'Modifié', + 'UI:CSVReport-Icon-Missing' => 'A disparu', + 'UI:CSVReport-Object-MissingToUpdate' => 'Objet disparu: sera modifié', + 'UI:CSVReport-Object-MissingUpdated' => 'Objet disparu: modifié', + 'UI:CSVReport-Icon-Created' => 'Créé', + 'UI:CSVReport-Object-ToCreate' => 'L\'objet sera créé', + 'UI:CSVReport-Object-Created' => 'Objet créé', + 'UI:CSVReport-Icon-Error' => 'Erreur', + 'UI:CSVReport-Object-Error' => 'Erreur: %1$s', + 'UI:CSVReport-Object-Ambiguous' => 'Ambigüité: %1$s', + 'UI:CSVReport-Stats-Errors' => '%1$.0f %% des lignes chargées sont en erreur et seront ignorées.', + 'UI:CSVReport-Stats-Created' => '%1$.0f %% des lignes chargées vont engendrer un nouvel objet.', + 'UI:CSVReport-Stats-Modified' => '%1$.0f %% des lignes chargées vont modifier un objet.', + + 'UI:CSVExport:AdvancedMode' => 'Mode expert', + 'UI:CSVExport:AdvancedMode+' => 'Dans le mode expert, des colonnes supplémentaires apparaissent: l\'identifiant de l\'objet, la valeur des clés externes et leurs attributs de reconciliation.', + 'UI:CSVExport:LostChars' => 'Problème d\'encodage', + 'UI:CSVExport:LostChars+' => 'Le fichier téléchargé sera encodé en %1$s. iTop a détecté des caractères incompatible avec ce format. Ces caractères seront soit remplacés par des caractères de substitution (par exemple: \'é\' transformé en \'e\'), soit perdus. Vous pouvez utiliser le copier/coller depuis votre navigateur web, ou bien contacter votre administrateur pour que l\'encodage corresponde mieux à votre besoin (Cf. paramètre \'csv_file_default_charset\').', + 'UI:UniversalSearchTitle' => 'iTop - Recherche Universelle', 'UI:UniversalSearch:Error' => 'Erreur : %1$s', 'UI:UniversalSearch:LabelSelectTheClass' => 'Sélectionnez le type d\'objets à rechercher : ', @@ -716,6 +763,7 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé 'UI:OpenDocumentInNewWindow_' => 'Ouvrir ce document dans uns autre fenêtre: %1$s', 'UI:DownloadDocument_' => 'Télécharger ce document: %1$s', 'UI:Document:NoPreview' => 'L\'aperçu n\'est pas disponible pour ce type de documents', + 'UI:Download-CSV' => 'Télécharger %1$s', 'UI:DeadlineMissedBy_duration' => 'Passé de %1$s', 'UI:Deadline_LessThan1Min' => '< 1 min', 'UI:Deadline_Minutes' => '%1$d min', diff --git a/pages/ajax.csvimport.php b/pages/ajax.csvimport.php index e1836f498..c27089e94 100644 --- a/pages/ajax.csvimport.php +++ b/pages/ajax.csvimport.php @@ -98,9 +98,10 @@ function GetMappingsForExtKey($sAttCode, AttributeDefinition $oExtKeyAttDef, $bA * @param string $sFieldName Name of the field, as it comes from the data file (header line) * @param integer $iFieldIndex Number of the field in the sequence * @param bool $bAdvancedMode Whether or not advanced mode was chosen + * @param string $sDefaultChoice If set, this will be the item selected by default * @return string The HTML code corresponding to the drop-down list for this field */ -function GetMappingForField($sClassName, $sFieldName, $iFieldIndex, $bAdvancedMode = false) +function GetMappingForField($sClassName, $sFieldName, $iFieldIndex, $bAdvancedMode, $sDefaultChoice) { $aChoices = array('' => Dict::S('UI:CSVImport:MappingSelectOne')); $aChoices[':none:'] = Dict::S('UI:CSVImport:MappingNotApplicable'); @@ -178,7 +179,7 @@ function GetMappingForField($sClassName, $sFieldName, $iFieldIndex, $bAdvancedMo } } asort($aChoices); - + $sHtml = "'); $oPage->add(''.(isset($aData[$iStartLine][$index-1]) ? htmlentities($aData[$iStartLine][$index-1], ENT_QUOTES, 'UTF-8') : ' ').''); @@ -343,28 +365,46 @@ try $index++; } $oPage->add("\n"); - $aReconciliationKeys = MetaModel::GetReconcKeys($sClassName); - $aMoreReconciliationKeys = array(); // Store: key => void to automatically remove duplicates - foreach($aReconciliationKeys as $sAttCode) + + if (empty($sInitSearchField)) { - if (!MetaModel::IsValidAttCode($sClassName, $sAttCode)) continue; - $oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCode); - if ($oAttDef->IsExternalKey()) + // Propose a reconciliation scheme + // + $aReconciliationKeys = MetaModel::GetReconcKeys($sClassName); + $aMoreReconciliationKeys = array(); // Store: key => void to automatically remove duplicates + foreach($aReconciliationKeys as $sAttCode) { - // An external key is specified as a reconciliation key: this means that all the reconciliation - // keys of this class are proposed to identify the target object - $aMoreReconciliationKeys = array_merge($aMoreReconciliationKeys, GetMappingsForExtKey($sAttCode, $oAttDef, $bAdvanced)); - } - elseif($oAttDef->IsExternalField()) - { - // An external field is specified as a reconciliation key, translate the field into a field on the target class - // since external fields are not writable, and thus never appears in the mapping form - $sKeyAttCode = $oAttDef->GetKeyAttCode(); - $sTargetAttCode = $oAttDef->GetExtAttCode(); - $aMoreReconciliationKeys[$sKeyAttCode.'->'.$sTargetAttCode] = ''; + if (!MetaModel::IsValidAttCode($sClassName, $sAttCode)) continue; + $oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCode); + if ($oAttDef->IsExternalKey()) + { + // An external key is specified as a reconciliation key: this means that all the reconciliation + // keys of this class are proposed to identify the target object + $aMoreReconciliationKeys = array_merge($aMoreReconciliationKeys, GetMappingsForExtKey($sAttCode, $oAttDef, $bAdvanced)); + } + elseif($oAttDef->IsExternalField()) + { + // An external field is specified as a reconciliation key, translate the field into a field on the target class + // since external fields are not writable, and thus never appears in the mapping form + $sKeyAttCode = $oAttDef->GetKeyAttCode(); + $sTargetAttCode = $oAttDef->GetExtAttCode(); + $aMoreReconciliationKeys[$sKeyAttCode.'->'.$sTargetAttCode] = ''; + } } + $sDefaultKeys = '"'.implode('", "',array_merge($aReconciliationKeys, array_keys($aMoreReconciliationKeys))).'"'; + } + else + { + // The reconciliation scheme is given (navigating back in the wizard) + // + $aDefaultKeys = array(); + foreach ($aInitSearchField as $iSearchField => $void) + { + $sAttCodeEx = $aInitFieldMapping[$iSearchField]; + $aDefaultKeys[] = $sAttCodeEx; + } + $sDefaultKeys = '"'.implode('", "', $aDefaultKeys).'"'; } - $sDefaultKeys = '"'.implode('", "',array_merge($aReconciliationKeys, array_keys($aMoreReconciliationKeys))).'"'; $oPage->add_ready_script( <<ToArray($iSkippedLines); + $iRealSkippedLines = $iSkippedLines; if ($bHeaderLine) { $aResult[] = $sTextQualifier.implode($sTextQualifier.$sSeparator.$sTextQualifier, array_shift($aData)).$sTextQualifier; // Remove the first line and store it in case of error @@ -311,7 +316,9 @@ try $aExtKeys, array_keys($aSearchKeys), empty($sSynchroScope) ? null : $sSynchroScope, - $aSynchroUpdate + $aSynchroUpdate, + null, // date format + true // localize ); $oBulk->SetReportHtml(); @@ -326,7 +333,7 @@ try { if (!empty($sAttCode) && ($sAttCode != ':none:') && ($sAttCode != 'finalclass')) { - $sHtml .= "".BulkChange::GetFriendlyAttCodeName($sClassName, $sAttCode).""; + $sHtml .= "".MetaModel::GetLabel($sClassName, $sAttCode).""; } } $sHtml .= 'Message'; @@ -351,7 +358,7 @@ try $sFinalClass = $aResRow['finalclass']; $oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue()); $sUrl = $oObj->GetHyperlink(); - $sStatus = ''; + $sStatus = ''; $sCSSRowClass = 'row_unchanged'; break; @@ -360,7 +367,7 @@ try $sFinalClass = $aResRow['finalclass']; $oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue()); $sUrl = $oObj->GetHyperlink(); - $sStatus = ''; + $sStatus = ''; $sCSSRowClass = 'row_modified'; break; @@ -369,40 +376,40 @@ try $sFinalClass = $aResRow['finalclass']; $oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue()); $sUrl = $oObj->GetHyperlink(); - $sStatus = ''; + $sStatus = ''; $sCSSRowClass = 'row_modified'; if ($bSimulate) { - $sMessage = 'Missing object: will be updated'; + $sMessage = Dict::S('UI:CSVReport-Object-MissingToUpdate'); } else { - $sMessage = 'Missing object: updated'; + $sMessage = Dict::S('UI:CSVReport-Object-MissingUpdated'); } break; case 'RowStatus_NewObj': $iCreated++; $sFinalClass = $aResRow['finalclass']; - $sStatus = ''; + $sStatus = ''; $sCSSRowClass = 'row_added'; if ($bSimulate) { - $sMessage = 'Object will be created'; + $sMessage = Dict::S('UI:CSVReport-Object-ToCreate'); } else { $sFinalClass = $aResRow['finalclass']; $oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue()); $sUrl = $oObj->GetHyperlink(); - $sMessage = 'Object created'; + $sMessage = Dict::S('UI:CSVReport-Object-Created'); } break; case 'RowStatus_Issue': $iErrors++; $sMessage .= $oPage->GetP($oStatus->GetDescription()); - $sStatus = ''; + $sStatus = '';//translate $sCSSMessageClass = 'cell_error'; $sCSSRowClass = 'row_error'; if (array_key_exists($iLine, $aData)) @@ -447,7 +454,7 @@ try { case 'CellStatus_Issue': $sCellMessage .= $oPage->GetP($oCellStatus->GetDescription()); - $sHtml .= 'ERROR: '.$sHtmlValue.$sCellMessage.''; + $sHtml .= ''.Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue).$sCellMessage.''; break; case 'CellStatus_SearchIssue': @@ -457,7 +464,7 @@ try case 'CellStatus_Ambiguous': $sCellMessage .= $oPage->GetP($oCellStatus->GetDescription()); - $sHtml .= 'AMBIGUOUS: '.$sHtmlValue.$sCellMessage.''; + $sHtml .= ''.Dict::Format('UI:CSVReport-Object-Ambiguous', $sHtmlValue).$sCellMessage.''; break; case 'CellStatus_Modify': @@ -481,8 +488,8 @@ try $oPage->add(''); $oPage->add(''); $oPage->add(''); - $oPage->add(''); - $oPage->add(''); + $oPage->add(''); + $oPage->add(''); $oPage->add(''); $oPage->add(''); $oPage->add(''); @@ -542,19 +549,19 @@ try $fErrorsPercentage = (100.0*$iErrors)/count($aRes); if ($fErrorsPercentage >= MetaModel::GetConfig()->Get('csv_import_errors_percentage')) { - $sMessage = sprintf("%.0f %% of the loaded objects have errors and will be ignored.", $fErrorsPercentage); + $sMessage = Dict::Format('UI:CSVReport-Stats-Errors', $fErrorsPercentage); $bShouldConfirm = true; } $fCreatedPercentage = (100.0*$iCreated)/count($aRes); if ($fCreatedPercentage >= MetaModel::GetConfig()->Get('csv_import_creations_percentage')) { - $sMessage = sprintf("%.0f %% of the loaded objects will be created.", $fCreatedPercentage); + $sMessage = Dict::Format('UI:CSVReport-Stats-Created', $fCreatedPercentage); $bShouldConfirm = true; } $fModifiedPercentage = (100.0*$iModified)/count($aRes); if ($fModifiedPercentage >= MetaModel::GetConfig()->Get('csv_import_modifications_percentage')) { - $sMessage = sprintf("%.0f %% of the loaded objects will be modified.", $fModifiedPercentage); + $sMessage = Dict::Format('UI:CSVReport-Stats-Modified', $fModifiedPercentage); $bShouldConfirm = true; } @@ -799,11 +806,6 @@ EOF $sTextQualifier = utils::ReadParam('other_qualifier', '"', false, 'raw_data'); } $bHeaderLine = (utils::ReadParam('header_line', '0') == 1); - $iSkippedLines = 0; - if (utils::ReadParam('box_skiplines', '0') == 1) - { - $iSkippedLines = utils::ReadParam('nb_skipped_lines', '0'); - } $sClassName = utils::ReadParam('class_name', '', false, 'class'); $bAdvanced = utils::ReadParam('advanced', 0); $sEncoding = utils::ReadParam('encoding', 'UTF-8'); @@ -835,8 +837,8 @@ EOF $oPage->add(''); $oPage->add(''); $oPage->add(''); - $oPage->add(''); - $oPage->add(''); + $oPage->add(''); + $oPage->add(''); $oPage->add(''); $oPage->add(''); $oPage->add(''); @@ -859,12 +861,17 @@ EOF $oPage->add_ready_script( <<add_ready_script("DoMapping();"); // There is already a class selected, run the mapping + $aFieldsMapping = utils::ReadParam('field', array(), false, 'raw_data'); + $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( @@ -889,7 +896,7 @@ EOF var ajax_request = null; - function DoMapping() + function DoMapping(sInitFieldsMapping, sInitSearchFields) { var class_name = $('select[name=class_name]').val(); var advanced = $('input[name=advanced]:checked').val(); @@ -906,7 +913,11 @@ EOF var separator = $('input[name=separator]').val(); var text_qualifier = $('input[name=text_qualifier]').val(); var header_line = $('input[name=header_line]').val(); - var nb_lines_skipped = $('input[name=nb_skipped_lines]').val(); + var do_skip_lines = 0; + if ($('input[name=box_skiplines]').val() == '1') + { + do_skip_lines = $('input[name=nb_skipped_lines]').val(); + } var csv_data = $('input[name=csvdata]').val(); var encoding = $('input[name=encoding]').val(); if (advanced != 1) @@ -922,11 +933,19 @@ EOF ajax_request.abort(); ajax_request = null; } - + + var aParams = { operation: 'display_mapping_form', enctype: 'multipart/form-data', csvdata: csv_data, separator: separator, + qualifier: text_qualifier, do_skip_lines: do_skip_lines, header_line: header_line, class_name: class_name, + advanced: advanced, encoding: encoding }; + + if (sInitFieldsMapping != undefined) + { + aParams.init_field_mapping = sInitFieldsMapping; + aParams.init_search_field = sInitSearchFields; + } + ajax_request = $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.csvimport.php', - { operation: 'display_mapping_form', enctype: 'multipart/form-data', csvdata: csv_data, separator: separator, - qualifier: text_qualifier, nb_lines_skipped: nb_lines_skipped, header_line: header_line, class_name: class_name, - advanced: advanced, encoding: encoding }, + aParams, function(data) { $('#mapping').empty(); $('#mapping').append(data); @@ -1067,6 +1086,13 @@ EOF // Compute a subset of the data set, now that we know the charset if ($sEncoding == 'UTF-8') { + // Remove the BOM if any + if (substr($sCSVData, 0, 3) == UTF8_BOM) + { + $sCSVData = substr($sCSVData, 3); + } + // Clean the input + // Todo: warn the user if some characters are lost/substituted $sUTF8Data = iconv('UTF-8', 'UTF-8//IGNORE//TRANSLIT', $sCSVData); } else @@ -1094,6 +1120,8 @@ EOF $bHeaderLine = utils::ReadParam('header_line', 0); $sClassName = utils::ReadParam('class_name', '', false, 'class'); $bAdvanced = utils::ReadParam('advanced', 0); + $aFieldsMapping = utils::ReadParam('field', array(), false, 'raw_data'); + $aSearchFields = utils::ReadParam('search_field', array(), false, 'field_name'); // 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 @@ -1157,9 +1185,19 @@ EOF $oPage->add(''); $oPage->add(''); $oPage->add(''); + // The encoding has changed, keep that information within the wizard + $oPage->add(''); $oPage->add(''); $oPage->add(''); $oPage->add(''); + foreach($aFieldsMapping as $iNumber => $sAttCode) + { + $oPage->add(''); + } + foreach($aSearchFields as $index => $sDummy) + { + $oPage->add(''); + } $oPage->add(''); if (!empty($sSynchroScope)) { @@ -1199,10 +1237,10 @@ EOF { text_qualifier = $('#other_qualifier').val(); } - var nb_lines_skipped = 0; + var do_skip_lines = 0; if ($('#box_skiplines:checked').val() != null) { - nb_lines_skipped = $('#nb_skipped_lines').val(); + do_skip_lines = $('#nb_skipped_lines').val(); } var header_line = 0; if ($('#box_header:checked').val() != null) @@ -1222,7 +1260,7 @@ EOF } ajax_request = $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.csvimport.php', - { operation: 'parser_preview', enctype: 'multipart/form-data', csvdata: $("#csvdata_truncated").val(), separator: separator, qualifier: text_qualifier, nb_lines_skipped: nb_lines_skipped, header_line: header_line, encoding: encoding }, + { operation: 'parser_preview', enctype: 'multipart/form-data', csvdata: $("#csvdata_truncated").val(), separator: separator, qualifier: text_qualifier, do_skip_lines: do_skip_lines, header_line: header_line, encoding: encoding }, function(data) { $('#preview').empty(); $('#preview').append(data); @@ -1280,11 +1318,16 @@ EOF $sSeparator = utils::ReadParam('separator', '', false, 'raw_data'); $sTextQualifier = utils::ReadParam('text_qualifier', '', false, 'raw_data'); $bHeaderLine = utils::ReadParam('header_line', true); - $iSkippedLines = utils::ReadParam('nb_skipped_lines', ''); $sClassName = utils::ReadParam('class_name', ''); $bAdvanced = utils::ReadParam('advanced', 0); - $sEncoding = utils::ReadParam('encoding', 'UTF-8'); - + $sEncoding = utils::ReadParam('encoding', ''); + if ($sEncoding == '') + { + $sEncoding = MetaModel::GetConfig()->Get('csv_file_default_charset'); + } + $aFieldsMapping = utils::ReadParam('field', array(), false, 'raw_data'); + $aSearchFields = utils::ReadParam('search_field', array(), false, 'field_name'); + $sFileLoadHtml = '

'.Dict::S('UI:CSVImport:SelectFile').'

'. '

'; @@ -1304,7 +1347,8 @@ EOF ''. ''. ''. - ''. + ''. + ''. ''. ''. ''; @@ -1315,6 +1359,15 @@ EOF $sFileLoadHtml .= ''; } } + foreach($aFieldsMapping as $iNumber => $sAttCode) + { + $oPage->add(''); + } + foreach($aSearchFields as $index => $sDummy) + { + $oPage->add(''); + } + $sFileLoadHtml .= '
'; $oPage->AddToTab('tabs1', Dict::S('UI:CSVImport:Tab:LoadFromFile'), $sFileLoadHtml); @@ -1339,7 +1392,8 @@ EOF ''. ''. ''. - ''. + ''. + ''. ''. ''. ''; @@ -1350,6 +1404,14 @@ EOF $sPasteDataHtml .= ''; } } + foreach($aFieldsMapping as $iNumber => $sAttCode) + { + $sPasteDataHtml .= ''; + } + foreach($aSearchFields as $index => $sDummy) + { + $sPasteDataHtml .= ''; + } $sPasteDataHtml .= ''; $oPage->AddToTab('tabs1', Dict::S('UI:CSVImport:Tab:CopyPaste'), $sPasteDataHtml); diff --git a/synchro/synchrodatasource.class.inc.php b/synchro/synchrodatasource.class.inc.php index 302b2095f..7f74c0d9b 100644 --- a/synchro/synchrodatasource.class.inc.php +++ b/synchro/synchrodatasource.class.inc.php @@ -2019,7 +2019,8 @@ class SynchroReplica extends DBObject implements iDisplay return null; } // MakeValueFromString() throws an exception in case of failure - $retValue = $oAttDef->MakeValueFromString($rawValue, $oSyncAtt->Get('row_separator'), $oSyncAtt->Get('attribute_separator'), $oSyncAtt->Get('value_separator'), $oSyncAtt->Get('attribute_qualifier')); + $bLocalizedValue = false; + $retValue = $oAttDef->MakeValueFromString($rawValue, $bLocalizedValue, $oSyncAtt->Get('row_separator'), $oSyncAtt->Get('attribute_separator'), $oSyncAtt->Get('value_separator'), $oSyncAtt->Get('attribute_qualifier')); } else { diff --git a/test/benchmark.php b/test/benchmark.php index 6fbc16899..61f4efa0f 100644 --- a/test/benchmark.php +++ b/test/benchmark.php @@ -148,7 +148,7 @@ class BenchmarkDataCreation // transform into a link set $sCSVSpec = implode('|', $value); $oAttDef = MetaModel::GetAttributeDef($sClass, $sProp); - $value = $oAttDef->MakeValueFromString($sCSVSpec, $sSepItem = '|', $sSepAttribute = ';', $sSepValue = ':', $sAttributeQualifier = '"'); + $value = $oAttDef->MakeValueFromString($sCSVSpec, $bLocalizedValue = false, $sSepItem = '|', $sSepAttribute = ';', $sSepValue = ':', $sAttributeQualifier = '"'); } $oMyObject->Set($sProp, $value); } diff --git a/test/testlist.inc.php b/test/testlist.inc.php index ef566c0e7..c62af6e4d 100644 --- a/test/testlist.inc.php +++ b/test/testlist.inc.php @@ -3251,7 +3251,7 @@ class TestSetLinkset extends TestBizModel $sLinkSetSpec = "profileid:10;reason:service manager|profileid->name:Problem Manager;'reason:problem manager;glandeur"; $oAttDef = MetaModel::GetAttributeDef('UserLocal', 'profile_list'); - $oSet = $oAttDef->MakeValueFromString($sLinkSetSpec); + $oSet = $oAttDef->MakeValueFromString($sLinkSetSpec, $bLocalizedValue = false); $oUser->Set('profile_list', $oSet); // Create a change to record the history of the User object diff --git a/webservices/export.php b/webservices/export.php index 342b28270..e2ec30281 100644 --- a/webservices/export.php +++ b/webservices/export.php @@ -69,14 +69,17 @@ else ApplicationContext::SetUrlMakerClass('iTopStandardURLMaker'); -$sOperation = utils::ReadParam('operation', 'menu'); $oAppContext = new ApplicationContext(); $iActiveNodeId = utils::ReadParam('menu', -1); $currentOrganization = utils::ReadParam('org_id', ''); +$bLocalize = (utils::ReadParam('no_localize', 0) != 1); +$sFileName = utils::ReadParam('filename', '', true, 'string'); + // Main program $sExpression = utils::ReadParam('expression', '', true /* Allow CLI */, 'raw_data'); $sFields = trim(utils::ReadParam('fields', '', true, 'raw_data')); // CSV field list (allows to specify link set attributes, still not taken into account for XML export) +$bFieldsAdvanced = utils::ReadParam('fields_advanced', 0); if (strlen($sExpression) == 0) { @@ -187,11 +190,11 @@ if (!empty($sExpression)) $bViewLink = false; } $sFields = implode(',', $aFields); - $aExtraParams = array('menu' => false, 'display_limit' => false, 'zlist' => false, 'extra_fields' => $sFields, 'view_link' => $bViewLink); + $aExtraParams = array('menu' => false, 'toolkit_menu' => false, 'display_limit' => false, 'localize_values' => $bLocalize, 'zlist' => false, 'extra_fields' => $sFields, 'view_link' => $bViewLink); } else { - $aExtraParams = array('menu' => false, 'display_limit' => false, 'zlist' => 'details'); + $aExtraParams = array('menu' => false, 'toolkit_menu' => false, 'display_limit' => false, 'localize_values' => $bLocalize, 'zlist' => 'details'); } $oResultBlock = new DisplayBlock($oFilter, 'list', false, $aExtraParams); @@ -201,7 +204,26 @@ if (!empty($sExpression)) case 'csv': $oP = new CSVPage("iTop - Export"); $sFields = implode(',', $aFields); - cmdbAbstractObject::DisplaySetAsCSV($oP, $oSet, array('fields' => $sFields)); + $sCSVData = cmdbAbstractObject::GetSetAsCSV($oSet, array('fields' => $sFields, 'fields_advanced' => $bFieldsAdvanced, 'localize_values' => $bLocalize)); + $sCharset = MetaModel::GetConfig()->Get('csv_file_default_charset'); + if ($sCharset == 'UTF-8') + { + $sOutputData = UTF8_BOM.iconv('UTF-8', 'UTF-8//IGNORE//TRANSLIT', $sCSVData); + } + else + { + $sOutputData = iconv('UTF-8', $sCharset.'//IGNORE//TRANSLIT', $sCSVData); + } + if ($sFileName == '') + { + // Plain text => Firefox will NOT propose to download the file + $oP->add_header("Content-type: text/plain; charset=$sCharset"); + } + else + { + $oP->add_header("Content-type: text/csv; charset=$sCharset"); + } + $oP->add($sOutputData); break; case 'spreadsheet': @@ -214,12 +236,12 @@ if (!empty($sExpression)) header("Cache-control:", true); $sFields = implode(',', $aFields); - cmdbAbstractObject::DisplaySetAsHTMLSpreadsheet($oP, $oSet, array('fields' => $sFields)); + cmdbAbstractObject::DisplaySetAsHTMLSpreadsheet($oP, $oSet, array('fields' => $sFields, 'fields_advanced' => $bFieldsAdvanced, 'localize_values' => $bLocalize)); break; case 'xml': $oP = new XMLPage("iTop - Export", true /* passthrough */); - cmdbAbstractObject::DisplaySetAsXML($oP, $oSet); + cmdbAbstractObject::DisplaySetAsXML($oP, $oSet, array('localize_values' => $bLocalize)); break; default: @@ -261,7 +283,13 @@ if (!$oP) $oP->p(" * arg_xxx: (needed if the query has parameters) the value of the parameter 'xxx'"); $oP->p(" * format: (optional, default is html) the desired output format. Can be one of 'html', 'spreadsheet', 'csv' or 'xml'"); $oP->p(" * fields: (optional, no effect on XML format) list of fields (attribute codes, or alias.attcode) separated by a coma"); + $oP->p(" * fields_advanced: (optional, no effect on XML format ; ignored is fields is specified) If set to 1, the default list of fields will include the external keys and their reconciliation keys"); + $oP->p(" * filename: (optional, no effect in CLI mode) if set then the results will be downloaded as a file"); } +if ($sFileName != '') +{ + $oP->add_header('Content-Disposition: attachment; filename="'.$sFileName.'"'); +} $oP->output(); ?> diff --git a/webservices/import.php b/webservices/import.php index 9760d8e82..4771a3e98 100644 --- a/webservices/import.php +++ b/webservices/import.php @@ -87,8 +87,8 @@ $aPageParams = array ( 'mandatory' => false, 'modes' => 'http,cli', - 'default' => 'UTF-8', - 'description' => 'Character set encoding of the CSV data: UTF-8, ISO-8859-1, WINDOWS-1251, WINDOWS-1252, ISO-8859-15', + 'default' => '', + 'description' => 'Character set encoding of the CSV data: UTF-8, ISO-8859-1, WINDOWS-1251, WINDOWS-1252, ISO-8859-15, If blank, then the charset is set to config(csv_file_default_charset)', ), 'date_format' => array ( @@ -148,6 +148,13 @@ $aPageParams = array 'default' => '', 'description' => 'Comment to be added into the change log', ), + 'no_localize' => array + ( + 'mandatory' => false, + 'modes' => 'http,cli', + 'default' => '0', + 'description' => 'If set to 0, then header and values are supposed to be localized in the language of the logged in user. Set to 1 to use internal attribute codes and values (enums)', + ), ); function UsageAndExit($oP) @@ -268,20 +275,22 @@ else try { + $aWarnings = array(); + ////////////////////////////////////////////////// // // Read parameters // - $sClass = ReadMandatoryParam($oP, 'class', 'class'); + $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'); $sDateFormat = ReadParam($oP, 'date_format', 'raw_data'); $sOutput = ReadParam($oP, 'output', 'string'); -// $sReportLevel = ReadParam($oP, 'reportlevel'); $sReconcKeys = ReadParam($oP, 'reconciliationkeys', 'raw_data'); $sSimulate = ReadParam($oP, 'simulate'); $sComment = ReadParam($oP, 'comment', 'raw_data'); + $bLocalize = (ReadParam($oP, 'no_localize') != 1); if (strtolower(trim($sSep)) == 'tab') { @@ -322,16 +331,10 @@ try $sDateFormat = null; } -/* - $aReportLevels = explode('|', $sReportLevel); - foreach($aReportLevels as $sLevel) + if ($sCharSet == '') { - if (!in_array($sLevel, explode('|', 'errors|warnings|created|changed|unchanged'))) - { - throw new BulkLoadException("Unknown level in reporting level: '$sLevel'"); - } + $sCharSet = MetaModel::GetConfig()->Get('csv_file_default_charset'); } -*/ if ($sSimulate == '1') { @@ -357,6 +360,7 @@ try { $oP->add_comment("Date format: "); } + $oP->add_comment("Localize: ".($bLocalize?'yes':'no')); $oP->add_comment("Data Size: ".strlen($sCSVData)); } ////////////////////////////////////////////////// @@ -370,24 +374,31 @@ try ////////////////////////////////////////////////// // - // Make translated column reference + // Create an index of the known column names (in lower case) + // If data is localized, an array of => array of (several leads to ambiguity) + // Otherwise an array of => array of (1 element by construction) // - // array of => - // - // Examples: - // 'organization' => 'org_id' - // 'organization->name' => 'org_id->name' + // Examples (localized in french): + // 'lieu' => 'location_id' + // 'lieu->name' => 'location_id->name' // // Note: it may happen that an external field has the same label as the external key // in that case, we consider that the external key has precedence // - $aFriendlyToInternalAttCode = array(); + $aKnownColumnNames = array(); foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) { - $sFriendlyName = strtolower(BulkChange::GetFriendlyAttCodeName($sClass, $sAttCode)); - if (!$oAttDef->IsExternalField() || !array_key_exists($sFriendlyName, $aFriendlyToInternalAttCode)) + if ($bLocalize) + { + $sColName = strtolower(MetaModel::GetLabel($sClass, $sAttCode)); + } + else { - $aFriendlyToInternalAttCode[$sFriendlyName] = $sAttCode; + $sColName = strtolower($sAttCode); + } + if (!$oAttDef->IsExternalField() || !array_key_exists($sColName, $aKnownColumnNames)) + { + $aKnownColumnNames[$sColName][] = $sAttCode; } if ($oAttDef->IsExternalKey(EXTKEY_RELATIVE)) { @@ -395,22 +406,40 @@ try foreach(MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef) { $sAttCodeEx = $sAttCode.'->'.$sRemoteAttCode; - $sFriendlyName = strtolower(BulkChange::GetFriendlyAttCodeName($sClass, $sAttCodeEx)); - if (!array_key_exists($sFriendlyName, $aFriendlyToInternalAttCode)) + if ($bLocalize) + { + $sColName = strtolower(MetaModel::GetLabel($sClass, $sAttCodeEx)); + } + else + { + $sColName = strtolower($sAttCodeEx); + } + if (!array_key_exists($sColName, $aKnownColumnNames)) { - $aFriendlyToInternalAttCode[$sFriendlyName] = $sAttCodeEx; + $aKnownColumnNames[$sColName][] = $sAttCodeEx; } } } } - + + //print_r($aKnownColumnNames); + //print_r(array_keys($aKnownColumnNames)); + //exit; + ////////////////////////////////////////////////// // // Parse first line, check attributes, analyse the request // if ($sCharSet == 'UTF-8') { - $sUTF8Data = $sCSVData; + // Remove the BOM if any + if (substr($sCSVData, 0, 3) == UTF8_BOM) + { + $sCSVData = substr($sCSVData, 3); + } + // Clean the input + // Todo: warn the user if some characters are lost/substituted + $sUTF8Data = iconv('UTF-8', 'UTF-8//IGNORE//TRANSLIT', $sCSVData); } else { @@ -433,15 +462,25 @@ try // Ignore any trailing "star" (*) that simply indicates a mandatory field $sFieldName = $aMatches[1]; } - if (array_key_exists(strtolower($sFieldName), $aFriendlyToInternalAttCode)) + if (array_key_exists(strtolower($sFieldName), $aKnownColumnNames)) { - $aFieldList[$iFieldId] = $aFriendlyToInternalAttCode[strtolower($sFieldName)]; + $aColumns = $aKnownColumnNames[strtolower($sFieldName)]; + if (count($aColumns) > 1) + { + $aCompetitors = array(); + foreach ($aColumns as $sAttCodeEx) + { + $aCompetitors[] = $sAttCodeEx; + } + $aWarnings[] = "Input column '$sFieldName' is ambiguous. Could be related to ".implode (' or ', $aCompetitors).". The first one will be used: ".$aColumns[0]; + } + $aFieldList[$iFieldId] = $aColumns[0]; } else { - // Secure the field names against XSS injection (no <> neither " chars) + // Protect against XSS injection $sSafeName = str_replace(array('"', '<', '>'), '', $sFieldName); - $aFieldList[$iFieldId] = $sSafeName; + throw new BulkLoadException("Unknown column: '$sSafeName'"); } } // Note: at this stage the list of fields is supposed to be made of attcodes (and the symbol '->') @@ -459,16 +498,19 @@ try $sRemoteAttCode = $aMatches[2]; if (!MetaModel::IsValidAttCode($sClass, $sExtKeyAttCode)) { + // Safety net - should not happen now that column names are checked against known names throw new BulkLoadException("Unknown attribute '$sExtKeyAttCode' (class: '$sClass')"); } $oAtt = MetaModel::GetAttributeDef($sClass, $sExtKeyAttCode); if (!$oAtt->IsExternalKey()) { + // Safety net - should not happen now that column names are checked against known names throw new BulkLoadException("Not an external key '$sExtKeyAttCode' (class: '$sClass')"); } $sTargetClass = $oAtt->GetTargetClass(); if (!MetaModel::IsValidAttCode($sTargetClass, $sRemoteAttCode)) { + // Safety net - should not happen now that column names are checked against known names throw new BulkLoadException("Unknown attribute '$sRemoteAttCode' (key: '$sExtKeyAttCode', class: '$sTargetClass')"); } $aExtKeys[$sExtKeyAttCode][$sRemoteAttCode] = $iFieldId; @@ -483,6 +525,7 @@ try // if (!MetaModel::IsValidAttCode($sClass, $sFieldName)) { + // Safety net - should not happen now that column names are checked against known names throw new BulkLoadException("Unknown attribute '$sFieldName' (class: '$sClass')"); } $oAtt = MetaModel::GetAttributeDef($sClass, $sFieldName); @@ -515,7 +558,14 @@ try { if (in_array($sReconcKeyAttCode, $aFieldList)) { - $aReconcSpec[] = $sReconcKeyAttCode; + if ($bLocalize) + { + $aReconcSpec[] = MetaModel::GetLabel($sClass, $sReconcKeyAttCode); + } + else + { + $aReconcSpec[] = $sReconcKeyAttCode; + } } } if (count($aReconcSpec) == 0) @@ -534,10 +584,26 @@ try $sReconcKey = trim($sReconcKey); if (empty($sReconcKey)) continue; // skip empty spec - if (array_key_exists(strtolower($sReconcKey), $aFriendlyToInternalAttCode)) + if (array_key_exists(strtolower($sReconcKey), $aKnownColumnNames)) { // Translate from a translated name to codes - $sReconcKey = $aFriendlyToInternalAttCode[strtolower($sReconcKey)]; + $aColumns = $aKnownColumnNames[strtolower($sReconcKey)]; + if (count($aColumns) > 1) + { + $aCompetitors = array(); + foreach ($aColumns as $sAttCodeEx) + { + $aCompetitors[] = $sAttCodeEx; + } + $aWarnings[] = "Reconciliation key '$sReconcKey' is ambiguous. Could be related to ".implode (' or ', $aCompetitors).". The first one will be used: ".$aColumns[0]; + } + $sReconcKey = $aColumns[0]; + } + else + { + // Protect against XSS injection + $sSafeName = str_replace(array('"', '<', '>'), '', $sReconcKey); + throw new BulkLoadException("Unknown reconciliation key: '$sSafeName'"); } // Check that the reconciliation key is either a given column, or an external key @@ -565,7 +631,7 @@ try { if (!MetaModel::IsValidAttCode($sClass, $sReconcKey)) { - // Safety net: should never happen, but... + // Safety net - should not happen now that column names are checked against known names throw new BulkLoadException("Unknown reconciliation attribute '$sReconcKey' (class: '$sClass')"); } $oAtt = MetaModel::GetAttributeDef($sClass, $sReconcKey); @@ -617,6 +683,11 @@ try } } $oP->add_comment("Reconciliation Keys: ".implode(', ', $aReconciliationReport)); + + foreach ($aWarnings as $sWarning) + { + $oP->add_comment("Warning: ".$sWarning); + } } $oBulk = new BulkChange( @@ -627,7 +698,8 @@ try $aFinalReconcilKeys, null, // synchro scope null, // on delete - $sDateFormat + $sDateFormat, + $bLocalize ); if ($bSimulate) @@ -717,7 +789,6 @@ try if (($sOutput == "summary") || ($sOutput == 'details')) { -// $oP->add_comment("Report level: ".$sReportLevel); $oP->add_comment("Change tracking comment: ".$sComment); $oP->add_comment("Issues: ".$iCountErrors); $oP->add_comment("Warnings: ".$iCountWarnings);