['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( <<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); } }