Files
iTop/core/tabularbulkexport.class.inc.php

453 lines
16 KiB
PHP

<?php
/*
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
use Combodo\iTop\Application\WebPage\WebPage;
/**
* Bulk export: Tabular export: abstract base class for all "tabular" exports.
* Provides the user interface for selecting the column to be exported
*
* @copyright Copyright (C) 2024 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
abstract class TabularBulkExport extends BulkExport
{
public function EnumFormParts()
{
return array_merge(parent::EnumFormParts(), ['tabular_fields' => ['fields']]);
}
/**
* @param WebPage $oP
* @param $sPartId
*
* @return UIContentBlock
*/
public function GetFormPart(WebPage $oP, $sPartId)
{
switch ($sPartId) {
case 'tabular_fields':
$sFields = utils::ReadParam('fields', '', true, 'raw_data');
$sSuggestedFields = utils::ReadParam('suggested_fields', null, true, 'raw_data');
if (($sSuggestedFields !== null) && ($sSuggestedFields !== '')) {
$aSuggestedFields = explode(',', $sSuggestedFields);
$sFields = implode(',', $this->SuggestFields($aSuggestedFields));
}
return InputUIBlockFactory::MakeForHidden("fields", $sFields, "tabular_fields");
break;
default:
return parent::GetFormPart($oP, $sPartId);
}
}
protected function SuggestFields($aSuggestedFields)
{
$aRet = [];
// By defaults all fields are Ok, nothing gets translated but
// you can overload this method if some fields are better exported
// (in a given format) by using an alternate field, for example id => friendlyname
$aAliases = $this->oSearch->GetSelectedClasses();
foreach ($aSuggestedFields as $idx => $sField) {
if (preg_match('/^([^\\.]+)\\.(.+)$/', $sField, $aMatches)) {
$sAlias = $aMatches[1];
$sAttCode = $aMatches[2];
$sClass = $aAliases[$sAlias];
} else {
$sAlias = '';
$sAttCode = $sField;
$sClass = reset($aAliases);
}
$sMostRelevantField = $this->SuggestField($sClass, $sAttCode);
$sAttCodeEx = MetaModel::NormalizeFieldSpec($sClass, $sMostRelevantField);
// Remove the aliases (if any) from the field names to make them compatible
// with the 'short' notation used in this case by the widget
if (count($aAliases) > 1) {
$sAttCodeEx = $sAlias.'.'.$sAttCodeEx;
}
$aRet[] = $sAttCodeEx;
}
return $aRet;
}
protected function SuggestField($sClass, $sAttCode)
{
return $sAttCode;
}
protected function IsSubAttribute($sClass, $sAttCode, $oAttDef)
{
return (($oAttDef instanceof AttributeFriendlyName) || ($oAttDef instanceof AttributeExternalField) || ($oAttDef instanceof AttributeSubItem));
}
protected function GetSubAttributes($sClass, $sAttCode, $oAttDef)
{
$aResult = [];
switch (get_class($oAttDef)) {
case 'AttributeExternalKey':
case 'AttributeHierarchicalKey':
$bAddFriendlyName = true;
$oKeyAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
$sRemoteClass = $oKeyAttDef->GetTargetClass();
$sFriendlyNameAttCode = MetaModel::GetFriendlyNameAttributeCode($sRemoteClass);
if (!is_null($sFriendlyNameAttCode)) {
// The friendly name is made of a single attribute, check if that attribute is present as an external field
foreach (MetaModel::ListAttributeDefs($sClass) as $sSubAttCode => $oSubAttDef) {
if ($oSubAttDef instanceof AttributeExternalField) {
if (($oSubAttDef->GetKeyAttCode() == $sAttCode) && ($oSubAttDef->GetExtAttCode() == $sFriendlyNameAttCode)) {
$bAddFriendlyName = false;
}
}
}
}
$aResult[$sAttCode] = ['code' => $sAttCode, 'unique_label' => $oAttDef->GetLabel(), 'label' => Dict::S('UI:CSVImport:idField'), 'attdef' => $oAttDef];
if ($bAddFriendlyName) {
if ($this->IsExportableField($sClass, $sAttCode.'->friendlyname')) {
$aResult[$sAttCode.'->friendlyname'] = ['code' => $sAttCode.'->friendlyname', 'unique_label' => $oAttDef->GetLabel().'->'.Dict::S('Core:FriendlyName-Label'), 'label' => Dict::S('Core:FriendlyName-Label'), 'attdef' => MetaModel::GetAttributeDef($sClass, $sAttCode.'_friendlyname')];
}
}
foreach (MetaModel::ListAttributeDefs($sClass) as $sSubAttCode => $oSubAttDef) {
if ($oSubAttDef instanceof AttributeExternalField) {
if ($this->IsExportableField($sClass, $sSubAttCode, $oSubAttDef)) {
if ($oSubAttDef->GetKeyAttCode() == $sAttCode) {
$sAttCodeEx = $sAttCode.'->'.$oSubAttDef->GetExtAttCode();
$aResult[$sAttCodeEx] = ['code' => $sAttCodeEx, 'unique_label' => $oAttDef->GetLabel().'->'.$oSubAttDef->GetExtAttDef()->GetLabel(), 'label' => MetaModel::GetLabel($sRemoteClass, $oSubAttDef->GetExtAttCode()), 'attdef' => $oSubAttDef];
}
}
}
}
// Add the reconciliation keys
foreach (MetaModel::GetReconcKeys($sRemoteClass) as $sRemoteAttCode) {
if (!empty($sRemoteAttCode)) {
$sAttCodeEx = $sAttCode.'->'.$sRemoteAttCode;
if (!array_key_exists($sAttCodeEx, $aResult)) {
$oRemoteAttDef = MetaModel::GetAttributeDef($sRemoteClass, $sRemoteAttCode);
if ($this->IsExportableField($sRemoteClass, $sRemoteAttCode, $oRemoteAttDef)) {
$aResult[$sAttCodeEx] = ['code' => $sAttCodeEx, 'unique_label' => $oAttDef->GetLabel().'->'.$oRemoteAttDef->GetLabel(), 'label' => MetaModel::GetLabel($sRemoteClass, $sRemoteAttCode), 'attdef' => $oRemoteAttDef];
}
}
}
}
break;
case 'AttributeStopWatch':
foreach (MetaModel::ListAttributeDefs($sClass) as $sSubAttCode => $oSubAttDef) {
if ($oSubAttDef instanceof AttributeSubItem) {
if ($oSubAttDef->GetParentAttCode() == $sAttCode) {
if ($this->IsExportableField($sClass, $sSubAttCode, $oSubAttDef)) {
$aResult[$sSubAttCode] = ['code' => $sSubAttCode, 'unique_label' => $oSubAttDef->GetLabel(), 'label' => $oSubAttDef->GetLabel(), 'attdef' => $oSubAttDef];
}
}
}
}
break;
}
return $aResult;
}
protected function GetInteractiveFieldsWidget(WebPage $oP, $sWidgetId)
{
$oSet = new DBObjectSet($this->oSearch);
$aSelectedClasses = $this->oSearch->GetSelectedClasses();
$aAuthorizedClasses = [];
foreach ($aSelectedClasses as $sAlias => $sClassName) {
if (UserRights::IsActionAllowed($sClassName, UR_ACTION_BULK_READ, $oSet) != UR_ALLOWED_NO) {
$aAuthorizedClasses[$sAlias] = $sClassName;
}
}
$aAllFieldsByAlias = [];
$aAllAttCodes = [];
foreach ($aAuthorizedClasses as $sAlias => $sClass) {
$aAllFields = [];
if (count($aAuthorizedClasses) > 1) {
$sShortAlias = $sAlias.'.';
} else {
$sShortAlias = '';
}
if ($this->IsExportableField($sClass, 'id')) {
$sFriendlyNameAttCode = MetaModel::GetFriendlyNameAttributeCode($sClass);
if (is_null($sFriendlyNameAttCode)) {
// The friendly name is made of several attribute
$aSubAttr = [
['attcodeex' => 'id', 'code' => $sShortAlias.'id', 'unique_label' => $sShortAlias.Dict::S('UI:CSVImport:idField'), 'label' => $sShortAlias.'id'],
['attcodeex' => 'friendlyname', 'code' => $sShortAlias.'friendlyname', 'unique_label' => $sShortAlias.Dict::S('Core:FriendlyName-Label'), 'label' => $sShortAlias.Dict::S('Core:FriendlyName-Label')],
];
} else {
// The friendly name has no added value
$aSubAttr = [];
}
$aAllFields[] = ['attcodeex' => 'id', 'code' => $sShortAlias.'id', 'unique_label' => $sShortAlias.Dict::S('UI:CSVImport:idField'), 'label' => Dict::S('UI:CSVImport:idField'), 'subattr' => $aSubAttr];
}
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) {
if ($this->IsSubAttribute($sClass, $sAttCode, $oAttDef)) {
continue;
}
if ($this->IsExportableField($sClass, $sAttCode, $oAttDef)) {
$sShortLabel = $oAttDef->GetLabel();
$sLabel = $sShortAlias.$oAttDef->GetLabel();
$aSubAttr = $this->GetSubAttributes($sClass, $sAttCode, $oAttDef);
$aValidSubAttr = [];
foreach ($aSubAttr as $aSubAttDef) {
$aValidSubAttr[] = ['attcodeex' => $aSubAttDef['code'], 'code' => $sShortAlias.$aSubAttDef['code'], 'label' => $aSubAttDef['label'], 'unique_label' => $sShortAlias.$aSubAttDef['unique_label']];
}
$aAllFields[] = ['attcodeex' => $sAttCode, 'code' => $sShortAlias.$sAttCode, 'label' => $sShortLabel, 'unique_label' => $sLabel, 'subattr' => $aValidSubAttr];
}
}
usort($aAllFields, [get_class($this), 'SortOnLabel']);
if (count($aAuthorizedClasses) > 1) {
$sKey = MetaModel::GetName($sClass).' ('.$sAlias.')';
} else {
$sKey = MetaModel::GetName($sClass);
}
$aAllFieldsByAlias[$sKey] = $aAllFields;
foreach ($aAllFields as $aFieldSpec) {
$sAttCode = $aFieldSpec['attcodeex'];
if (count($aFieldSpec['subattr']) > 0) {
foreach ($aFieldSpec['subattr'] as $aSubFieldSpec) {
$aAllAttCodes[$sAlias][] = $aSubFieldSpec['attcodeex'];
}
} else {
$aAllAttCodes[$sAlias][] = $sAttCode;
}
}
}
$JSAllFields = json_encode($aAllFieldsByAlias);
// First, fetch only the ids - the rest will be fetched by an object reload
$oSet = new DBObjectSet($this->oSearch);
$iCount = $oSet->Count();
foreach ($this->oSearch->GetSelectedClasses() as $sAlias => $sClass) {
$aColumns[$sAlias] = [];
}
$oSet->OptimizeColumnLoad($aColumns);
$iPreviewLimit = 3;
$oSet->SetLimit($iPreviewLimit);
$aSampleData = [];
while ($aRow = $oSet->FetchAssoc()) {
$aSampleRow = [];
foreach ($aAuthorizedClasses as $sAlias => $sClass) {
if (count($aAuthorizedClasses) > 1) {
$sShortAlias = $sAlias.'.';
} else {
$sShortAlias = '';
}
if (isset($aAllAttCodes[$sAlias])) {
foreach ($aAllAttCodes[$sAlias] as $sAttCodeEx) {
$oObj = $aRow[$sAlias];
$aSampleRow[$sShortAlias.$sAttCodeEx] = $oObj ? $this->GetSampleData($oObj, $sAttCodeEx) : '';
}
}
}
$aSampleData[] = $aSampleRow;
}
$sJSSampleData = json_encode($aSampleData);
$aLabels = [
'preview_header' => Dict::S('Core:BulkExport:DragAndDropHelp'),
'empty_preview' => Dict::S('Core:BulkExport:EmptyPreview'),
'columns_order' => Dict::S('Core:BulkExport:ColumnsOrder'),
'columns_selection' => Dict::S('Core:BulkExport:AvailableColumnsFrom_Class'),
'check_all' => Dict::S('Core:BulkExport:CheckAll'),
'uncheck_all' => Dict::S('Core:BulkExport:UncheckAll'),
'no_field_selected' => Dict::S('Core:BulkExport:NoFieldSelected'),
];
$sJSLabels = json_encode($aLabels);
$oP->add_ready_script(
<<<EOF
$('#$sWidgetId').tabularfieldsselector({fields: $JSAllFields, value_holder: '#tabular_fields', advanced_holder: '#tabular_advanced', sample_data: $sJSSampleData, total_count: $iCount, preview_limit: $iPreviewLimit, labels: $sJSLabels });
EOF
);
$oUIContentBlock = UIContentBlockUIBlockFactory::MakeStandard($sWidgetId);
$oUIContentBlock->AddCSSClass('ibo-tabularbulkexport');
return $oUIContentBlock;
}
public static function SortOnLabel($aItem1, $aItem2)
{
return strcmp($aItem1['label'], $aItem2['label']);
}
/**
* Tells if the specified field can be exported
* @param unknown $sClass
* @param unknown $sAttCode
* @param AttributeDefinition $oAttDef Can be null in case the attribute definition has not been fetched by the caller
* @return boolean
*/
protected function IsExportableField($sClass, $sAttCode, $oAttDef = null)
{
if ($sAttCode == 'id') {
return true;
}
if (is_null($oAttDef)) {
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
}
if ($oAttDef instanceof AttributeLinkedSet) {
return false;
}
return true; //$oAttDef->IsScalar();
}
protected function GetSampleData($oObj, $sAttCode)
{
if ($sAttCode == 'id') {
return $oObj->GetKey();
}
return $oObj->GetEditValue($sAttCode);
}
public function ReadParameters()
{
parent::ReadParameters();
$sQueryId = utils::ReadParam('query', null, true);
$sFields = utils::ReadParam('fields', null, true, 'raw_data');
if ((($sFields === null) || ($sFields === '')) && ($sQueryId === null)) {
throw new BulkExportMissingParameterException('fields');
} else {
if (($sQueryId !== null) && ($sQueryId !== null)) {
$oSearch = DBObjectSearch::FromOQL('SELECT QueryOQL WHERE id = :query_id', ['query_id' => $sQueryId]);
$oQueries = new DBObjectSet($oSearch);
if ($oQueries->Count() > 0) {
$oQuery = $oQueries->Fetch();
if (($sFields === null) || ($sFields === '')) {
// No 'fields' parameter supplied, take the fields from the query phrasebook definition
$sFields = trim($oQuery->Get('fields'));
if ($sFields === '') {
throw new BulkExportMissingParameterException('fields');
}
}
} else {
throw BulkExportException('Invalid value for the parameter: query. There is no Query Phrasebook with id = '.$sQueryId, Dict::Format('Core:BulkExport:InvalidParameter_Query', $sQueryId));
}
}
}
$this->SetFields($sFields);
}
public function SetFields($sFields)
{
// Interpret (and check) the list of fields
//
$aSelectedClasses = $this->oSearch->GetSelectedClasses();
$aAliases = array_keys($aSelectedClasses);
$aAuthorizedClasses = [];
foreach ($aSelectedClasses as $sAlias => $sClassName) {
if (UserRights::IsActionAllowed($sClassName, UR_ACTION_BULK_READ) == UR_ALLOWED_YES) {
$aAuthorizedClasses[$sAlias] = $sClassName;
}
}
$aFields = explode(',', $sFields);
$this->aStatusInfo['fields'] = [];
foreach ($aFields as $sFieldSpec) {
// Trim the values since it's natural to write: fields=name, first_name, org_name instead of fields=name,first_name,org_name
$sExtendedAttCode = trim($sFieldSpec);
if (preg_match('/^([^\.]+)\.(.+)$/', $sExtendedAttCode, $aMatches)) {
$sAlias = $aMatches[1];
$sAttCode = $aMatches[2];
} else {
$sAlias = reset($aAliases);
$sAttCode = $sExtendedAttCode;
}
if (!array_key_exists($sAlias, $aSelectedClasses)) {
throw new Exception("Invalid alias '$sAlias' for the column '$sExtendedAttCode'. Availables aliases: '".implode("', '", $aAliases)."'");
}
$sClass = $aSelectedClasses[$sAlias];
if (!array_key_exists($sAlias, $aAuthorizedClasses)) {
throw new Exception("You do not have enough permissions to bulk read data of class '$sClass' (alias: $sAlias)");
}
if ($this->bLocalizeOutput) {
try {
$sLabel = MetaModel::GetLabel($sClass, $sAttCode);
} catch (Exception $e) {
throw new Exception("Wrong field specification '$sFieldSpec': ".$e->getMessage());
}
} else {
$sLabel = $sAttCode;
}
if (count($aAuthorizedClasses) > 1) {
$sColLabel = $sAlias.'.'.$sLabel;
} else {
$sColLabel = $sLabel;
}
$this->aStatusInfo['fields'][] = [
'sFieldSpec' => $sExtendedAttCode,
'sAlias' => $sAlias,
'sClass' => $sClass,
'sAttCode' => $sAttCode,
'sLabel' => $sLabel,
'sColLabel' => $sColLabel,
];
}
}
/**
* Prepare the given object set with the list of fields as read into $this->aStatusInfo['fields']
*/
protected function OptimizeColumnLoad(DBObjectSet $oSet)
{
$aColumnsToLoad = [];
foreach ($this->aStatusInfo['fields'] as $iCol => $aFieldSpec) {
$sClass = $aFieldSpec['sClass'];
$sAlias = $aFieldSpec['sAlias'];
$sAttCode = $aFieldSpec['sAttCode'];
if (!array_key_exists($sAlias, $aColumnsToLoad)) {
$aColumnsToLoad[$sAlias] = [];
}
// id is not a real attribute code and, moreover, is always loaded
if ($sAttCode != 'id') {
// Extended attributes are not recognized by DBObjectSet::OptimizeColumnLoad
if (($iPos = strpos($sAttCode, '->')) === false) {
$aColumnsToLoad[$sAlias][] = $sAttCode;
$sClass = '???';
} else {
$sExtKeyAttCode = substr($sAttCode, 0, $iPos);
$sRemoteAttCode = substr($sAttCode, $iPos + 2);
// Load the external key to avoid an object reload!
$aColumnsToLoad[$sAlias][] = $sExtKeyAttCode;
// Load the external field (if any) to avoid getting the remote object (see DBObject::Get that does the same)
$oExtFieldAtt = MetaModel::FindExternalField($sClass, $sExtKeyAttCode, $sRemoteAttCode);
if (!is_null($oExtFieldAtt)) {
$aColumnsToLoad[$sAlias][] = $oExtFieldAtt->GetCode();
}
}
}
}
// Add "always loaded attributes"
//
$aSelectedClasses = $this->oSearch->GetSelectedClasses();
foreach ($aSelectedClasses as $sAlias => $sClass) {
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) {
if ($oAttDef->AlwaysLoadInTables()) {
$aColumnsToLoad[$sAlias][] = $sAttCode;
}
}
}
$oSet->OptimizeColumnLoad($aColumnsToLoad);
}
}