mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-29 13:38:44 +02:00
WIP
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Combodo\iTop\Application\WebPage\Page;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
|
||||
/**
|
||||
* Class BulkExport
|
||||
*
|
||||
@@ -193,7 +196,7 @@ abstract class BulkExport
|
||||
}
|
||||
|
||||
|
||||
public function SetHttpHeaders(\Combodo\iTop\Application\WebPage\WebPage $oPage)
|
||||
public function SetHttpHeaders(WebPage $oPage)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -254,7 +257,7 @@ abstract class BulkExport
|
||||
/**
|
||||
* @deprecated 3.0.0 use GetFormPart instead
|
||||
*/
|
||||
public function DisplayFormPart(\Combodo\iTop\Application\WebPage\WebPage $oP, $sPartId)
|
||||
public function DisplayFormPart(WebPage $oP, $sPartId)
|
||||
{
|
||||
DeprecatedCallsLog::NotifyDeprecatedPhpMethod('use GetFormPart instead');
|
||||
$oP->AddSubBlock($this->GetFormPart($oP, $sPartId));
|
||||
@@ -267,11 +270,11 @@ abstract class BulkExport
|
||||
*
|
||||
* @return UIContentBlock
|
||||
*/
|
||||
public function GetFormPart(\Combodo\iTop\Application\WebPage\WebPage $oP, $sPartId)
|
||||
public function GetFormPart(WebPage $oP, $sPartId)
|
||||
{
|
||||
}
|
||||
|
||||
public function DisplayUsage(\Combodo\iTop\Application\WebPage\Page $oP)
|
||||
public function DisplayUsage(Page $oP)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
410
sources/Application/BulkExport/excelbulkexport.class.inc.php
Normal file
410
sources/Application/BulkExport/excelbulkexport.class.inc.php
Normal file
@@ -0,0 +1,410 @@
|
||||
<?php
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\Component\FieldSet\FieldSetUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\MultiColumn\Column\ColumnUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\MultiColumn\MultiColumnUIBlockFactory;
|
||||
use Combodo\iTop\Application\Helper\ExportHelper;
|
||||
use Combodo\iTop\Application\WebPage\Page;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
|
||||
require_once(APPROOT.'application/xlsxwriter.class.php');
|
||||
|
||||
class ExcelBulkExport extends TabularBulkExport
|
||||
{
|
||||
protected $sData;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->aStatusInfo['status'] = 'not_started';
|
||||
$this->aStatusInfo['position'] = 0;
|
||||
}
|
||||
|
||||
public function Cleanup()
|
||||
{
|
||||
@unlink($this->aStatusInfo['tmp_file']);
|
||||
parent::Cleanup();
|
||||
}
|
||||
|
||||
public function DisplayUsage(Page $oP)
|
||||
{
|
||||
$oP->p(" * xlsx format options:");
|
||||
$oP->p(" *\tfields: the comma separated list of field codes to export (e.g: name,org_id,service_name...).");
|
||||
$oP->p(" *\tformatted_text: set to 1 to export case logs and formatted text fields with their HTML markup. Default is 0 (= plain text)");
|
||||
$oP->p(" *\tdate_format: the format to use when exporting date and time fields (default = the SQL format). e.g. 'Y-m-d H:i:s'");
|
||||
}
|
||||
|
||||
public function ReadParameters()
|
||||
{
|
||||
parent::ReadParameters();
|
||||
$this->aStatusInfo['formatted_text'] = (bool)utils::ReadParam('formatted_text', 0, true);
|
||||
|
||||
$sDateFormatRadio = utils::ReadParam('excel_date_format_radio', '');
|
||||
switch($sDateFormatRadio)
|
||||
{
|
||||
case 'default':
|
||||
// Export from the UI => format = same as is the UI
|
||||
$this->aStatusInfo['date_format'] = (string)AttributeDateTime::GetFormat();
|
||||
break;
|
||||
|
||||
case 'custom':
|
||||
// Custom format specified from the UI
|
||||
$this->aStatusInfo['date_format'] = utils::ReadParam('date_format', (string)AttributeDateTime::GetFormat(), true, 'raw_data');
|
||||
break;
|
||||
|
||||
default:
|
||||
// Export from the command line (or scripted) => default format is SQL, as in previous versions of iTop, unless specified otherwise
|
||||
$this->aStatusInfo['date_format'] = utils::ReadParam('date_format', (string)AttributeDateTime::GetSQLFormat(), true, 'raw_data');
|
||||
}
|
||||
}
|
||||
|
||||
public function EnumFormParts()
|
||||
{
|
||||
return array_merge(parent::EnumFormParts(), array('xlsx_options' => array('formatted_text'), 'interactive_fields_xlsx' => array('interactive_fields_xlsx')));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oP
|
||||
* @param $sPartId
|
||||
*
|
||||
* @return UIContentBlock
|
||||
*/
|
||||
public function GetFormPart(WebPage $oP, $sPartId)
|
||||
{
|
||||
switch ($sPartId) {
|
||||
case 'interactive_fields_xlsx':
|
||||
return $this->GetInteractiveFieldsWidget($oP, 'interactive_fields_xlsx');
|
||||
break;
|
||||
|
||||
case 'xlsx_options':
|
||||
$oPanel = PanelUIBlockFactory::MakeNeutral(Dict::S('Core:BulkExport:XLSXOptions'));
|
||||
$oPanel->AddSubBlock(ExportHelper::GetAlertForExcelMaliciousInjection());
|
||||
|
||||
$oMulticolumn = MultiColumnUIBlockFactory::MakeStandard();
|
||||
$oPanel->AddSubBlock($oMulticolumn);
|
||||
|
||||
$oFieldSetFormat = FieldSetUIBlockFactory::MakeStandard(Dict::S('Core:BulkExport:TextFormat'));
|
||||
$oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oFieldSetFormat));
|
||||
|
||||
$oCheckBox = InputUIBlockFactory::MakeForInputWithLabel(Dict::S('Core:BulkExport:OptionFormattedText'), "formatted_text", "1", "xlsx_formatted_text", "checkbox");
|
||||
$oCheckBox->GetInput()->SetIsChecked((utils::ReadParam('formatted_text', 0) == 1));
|
||||
$oCheckBox->SetBeforeInput(false);
|
||||
$oCheckBox->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||
$oFieldSetFormat->AddSubBlock($oCheckBox);
|
||||
|
||||
$oFieldSetDate = FieldSetUIBlockFactory::MakeStandard(Dict::S('Core:BulkExport:DateTimeFormat'));
|
||||
$oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oFieldSetDate));
|
||||
|
||||
$sDateTimeFormat = utils::ReadParam('date_format', (string)AttributeDateTime::GetFormat(), true, 'raw_data');
|
||||
|
||||
$sDefaultFormat = utils::EscapeHtml((string)AttributeDateTime::GetFormat());
|
||||
$sExample = utils::EscapeHtml(date((string)AttributeDateTime::GetFormat()));
|
||||
$oRadioDefault = InputUIBlockFactory::MakeForInputWithLabel(Dict::Format('Core:BulkExport:DateTimeFormatDefault_Example', $sDefaultFormat, $sExample), "excel_date_format_radio", "default", "excel_date_time_format_default", "radio");
|
||||
$oRadioDefault->GetInput()->SetIsChecked(($sDateTimeFormat == (string)AttributeDateTime::GetFormat()));
|
||||
$oRadioDefault->SetBeforeInput(false);
|
||||
$oRadioDefault->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||
$oFieldSetDate->AddSubBlock($oRadioDefault);
|
||||
$oFieldSetDate->AddSubBlock(new Html('</br>'));
|
||||
|
||||
$sFormatInput = '<input type="text" size="15" name="date_format" id="excel_custom_date_time_format" title="" value="'.utils::EscapeHtml($sDateTimeFormat).'"/>';
|
||||
$oRadioCustom = InputUIBlockFactory::MakeForInputWithLabel(Dict::Format('Core:BulkExport:DateTimeFormatCustom_Format', $sFormatInput), "excel_date_format_radio", "custom", "excel_date_time_format_custom", "radio");
|
||||
$oRadioCustom->SetDescription(Dict::S('UI:CSVImport:CustomDateTimeFormatTooltip'));
|
||||
$oRadioCustom->GetInput()->SetIsChecked($sDateTimeFormat !== (string)AttributeDateTime::GetFormat());
|
||||
$oRadioCustom->SetBeforeInput(false);
|
||||
$oRadioCustom->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||
$oFieldSetDate->AddSubBlock($oRadioCustom);
|
||||
|
||||
|
||||
$oP->add_ready_script(
|
||||
<<<EOF
|
||||
$('#form_part_xlsx_options').on('preview_updated', function() { FormatDatesInPreview('excel', 'xlsx'); });
|
||||
$('#excel_date_time_format_default').on('click', function() { FormatDatesInPreview('excel', 'xlsx'); });
|
||||
$('#excel_date_time_format_custom').on('click', function() { FormatDatesInPreview('excel', 'xlsx'); });
|
||||
$('#excel_custom_date_time_format').on('click', function() { $('#excel_date_time_format_custom').prop('checked', true); FormatDatesInPreview('excel', 'xlsx'); }).on('keyup', function() { FormatDatesInPreview('excel', 'xlsx'); });
|
||||
EOF
|
||||
);
|
||||
|
||||
return $oPanel;
|
||||
break;
|
||||
|
||||
default:
|
||||
return parent::GetFormPart($oP, $sPartId);
|
||||
}
|
||||
}
|
||||
|
||||
protected function SuggestField($sClass, $sAttCode)
|
||||
{
|
||||
switch($sAttCode)
|
||||
{
|
||||
case 'id': // replace 'id' by 'friendlyname'
|
||||
$sAttCode = 'friendlyname';
|
||||
break;
|
||||
|
||||
default:
|
||||
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
|
||||
if ($oAttDef instanceof AttributeExternalKey)
|
||||
{
|
||||
$sAttCode .= '_friendlyname';
|
||||
}
|
||||
}
|
||||
|
||||
return parent::SuggestField($sClass, $sAttCode);
|
||||
}
|
||||
|
||||
protected function GetSampleData($oObj, $sAttCode)
|
||||
{
|
||||
if ($sAttCode != 'id') {
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode);
|
||||
if ($oAttDef instanceof AttributeDateTime) // AttributeDate is derived from AttributeDateTime
|
||||
{
|
||||
$sClass = (get_class($oAttDef) == 'AttributeDateTime') ? 'user-formatted-date-time' : 'user-formatted-date';
|
||||
|
||||
return '<div class="'.$sClass.'" data-date="'.$oObj->Get($sAttCode).'">'.utils::EscapeHtml($oAttDef->GetEditValue($oObj->Get($sAttCode), $oObj)).'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
return '<div class="text-preview">'.utils::EscapeHtml($this->GetValue($oObj, $sAttCode)).'</div>';
|
||||
}
|
||||
|
||||
protected function GetValue($oObj, $sAttCode)
|
||||
{
|
||||
switch($sAttCode)
|
||||
{
|
||||
case 'id':
|
||||
$sRet = $oObj->GetKey();
|
||||
break;
|
||||
|
||||
default:
|
||||
$value = $oObj->Get($sAttCode);
|
||||
if ($value instanceOf ormCaseLog)
|
||||
{
|
||||
if (array_key_exists('formatted_text', $this->aStatusInfo) && $this->aStatusInfo['formatted_text'])
|
||||
{
|
||||
$sText = $value->GetText();
|
||||
}
|
||||
else
|
||||
{
|
||||
$sText = $value->GetAsPlainText();
|
||||
}
|
||||
// Extract the case log as text and remove the "===" which make Excel think that the cell contains a formula the next time you edit it!
|
||||
$sRet = trim(preg_replace('/========== ([^=]+) ============/', '********** $1 ************', $sText));
|
||||
}
|
||||
else if ($value instanceOf DBObjectSet)
|
||||
{
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode);
|
||||
$sRet = $oAttDef->GetAsCSV($value, '', '', $oObj);
|
||||
}
|
||||
else if ($value instanceOf ormDocument)
|
||||
{
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode);
|
||||
$sRet = $oAttDef->GetAsCSV($value, '', '', $oObj);
|
||||
}
|
||||
else if ($value instanceOf ormSet)
|
||||
{
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode);
|
||||
$sRet = $oAttDef->GetAsCSV($value, '', '', $oObj);
|
||||
}
|
||||
else
|
||||
{
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode);
|
||||
if ($oAttDef instanceof AttributeDateTime)
|
||||
{
|
||||
// Date and times are formatted using the ISO encoding, not the localized format
|
||||
if ($oAttDef->IsNull($value))
|
||||
{
|
||||
// NOt a valid date
|
||||
$sRet = '';
|
||||
}
|
||||
else
|
||||
{
|
||||
$sRet = $value;
|
||||
}
|
||||
}
|
||||
else if (array_key_exists('formatted_text', $this->aStatusInfo) && $this->aStatusInfo['formatted_text'])
|
||||
{
|
||||
if ($oAttDef instanceof AttributeText && $oAttDef->GetFormat()=='html')
|
||||
{
|
||||
$sRet = str_replace(">", ">", $value);
|
||||
}
|
||||
else
|
||||
{
|
||||
$sRet = $oAttDef->GetEditValue($value, $oObj);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$sRet = $oAttDef->GetAsPlainText($value, $oObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $sRet;
|
||||
}
|
||||
|
||||
public function GetHeader()
|
||||
{
|
||||
$oSet = new DBObjectSet($this->oSearch);
|
||||
$this->aStatusInfo['status'] = 'retrieving';
|
||||
$this->aStatusInfo['tmp_file'] = $this->MakeTmpFile('data');
|
||||
$this->aStatusInfo['position'] = 0;
|
||||
$this->aStatusInfo['total'] = $oSet->Count();
|
||||
|
||||
foreach($this->aStatusInfo['fields'] as $iCol => $aFieldSpec)
|
||||
{
|
||||
$sExtendedAttCode = $aFieldSpec['sFieldSpec'];
|
||||
$sAttCode = $aFieldSpec['sAttCode'];
|
||||
$sColLabel = $aFieldSpec['sColLabel'];
|
||||
|
||||
switch($sAttCode)
|
||||
{
|
||||
case 'id':
|
||||
$sType = '0';
|
||||
break;
|
||||
|
||||
default:
|
||||
$oAttDef = MetaModel::GetAttributeDef($aFieldSpec['sClass'], $aFieldSpec['sAttCode']);
|
||||
$sType = 'string';
|
||||
if($oAttDef instanceof AttributeDate)
|
||||
{
|
||||
$sType = 'date';
|
||||
}
|
||||
else if($oAttDef instanceof AttributeDateTime)
|
||||
{
|
||||
$sType = 'datetime';
|
||||
}
|
||||
}
|
||||
$aTableHeaders[] = array('label' => $sColLabel, 'type' => $sType);
|
||||
}
|
||||
|
||||
$sRow = json_encode($aTableHeaders);
|
||||
$hFile = @fopen($this->aStatusInfo['tmp_file'], 'ab');
|
||||
if ($hFile === false)
|
||||
{
|
||||
throw new Exception('ExcelBulkExport: Failed to open temporary data file: "'.$this->aStatusInfo['tmp_file'].'" for writing.');
|
||||
}
|
||||
fwrite($hFile, $sRow."\n");
|
||||
fclose($hFile);
|
||||
return '';
|
||||
}
|
||||
|
||||
public function GetNextChunk(&$aStatus)
|
||||
{
|
||||
$sRetCode = 'run';
|
||||
$iPercentage = 0;
|
||||
|
||||
$hFile = fopen($this->aStatusInfo['tmp_file'], 'ab');
|
||||
|
||||
$oSet = new DBObjectSet($this->oSearch);
|
||||
$oSet->SetLimit($this->iChunkSize, $this->aStatusInfo['position']);
|
||||
$this->OptimizeColumnLoad($oSet);
|
||||
|
||||
$iCount = 0;
|
||||
$iPreviousTimeLimit = ini_get('max_execution_time');
|
||||
$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
|
||||
while($aRow = $oSet->FetchAssoc())
|
||||
{
|
||||
set_time_limit(intval($iLoopTimeLimit));
|
||||
$aData = array();
|
||||
foreach($this->aStatusInfo['fields'] as $iCol => $aFieldSpec)
|
||||
{
|
||||
$sAlias = $aFieldSpec['sAlias'];
|
||||
$sAttCode = $aFieldSpec['sAttCode'];
|
||||
|
||||
$oObj = $aRow[$sAlias];
|
||||
$sField = '';
|
||||
if ($oObj)
|
||||
{
|
||||
$sField = $this->GetValue($oObj, $sAttCode);
|
||||
}
|
||||
$aData[] = $sField;
|
||||
}
|
||||
fwrite($hFile, json_encode($aData)."\n");
|
||||
$iCount++;
|
||||
}
|
||||
set_time_limit(intval($iPreviousTimeLimit));
|
||||
$this->aStatusInfo['position'] += $this->iChunkSize;
|
||||
if ($this->aStatusInfo['total'] == 0)
|
||||
{
|
||||
$iPercentage = 100;
|
||||
$sRetCode = 'done'; // Next phase (GetFooter) will be to build the xlsx file
|
||||
}
|
||||
else
|
||||
{
|
||||
$iPercentage = floor(min(100.0, 100.0*$this->aStatusInfo['position']/$this->aStatusInfo['total']));
|
||||
}
|
||||
if ($iCount < $this->iChunkSize)
|
||||
{
|
||||
$sRetCode = 'done';
|
||||
}
|
||||
$aStatus = array('code' => $sRetCode, 'message' => Dict::S('Core:BulkExport:RetrievingData'), 'percentage' => $iPercentage);
|
||||
return ''; // The actual XLSX file is built in GetFooter();
|
||||
}
|
||||
|
||||
public function GetFooter()
|
||||
{
|
||||
$hFile = @fopen($this->aStatusInfo['tmp_file'], 'rb');
|
||||
if ($hFile === false)
|
||||
{
|
||||
throw new Exception('ExcelBulkExport: Failed to open temporary data file: "'.$this->aStatusInfo['tmp_file'].'" for reading.');
|
||||
}
|
||||
$sHeaders = fgets($hFile);
|
||||
$aHeaders = json_decode($sHeaders, true);
|
||||
|
||||
$aData = array();
|
||||
while($sLine = fgets($hFile))
|
||||
{
|
||||
$aRow = json_decode($sLine);
|
||||
$aData[] = $aRow;
|
||||
}
|
||||
fclose($hFile);
|
||||
|
||||
$fStartExcel = microtime(true);
|
||||
$writer = new XLSXWriter();
|
||||
$sDateFormat = isset($this->aStatusInfo['date_format']) ? $this->aStatusInfo['date_format'] : (string)AttributeDateTime::GetFormat();
|
||||
$oDateTimeFormat = new DateTimeFormat($sDateFormat);
|
||||
$writer->setDateTimeFormat($oDateTimeFormat->ToExcel());
|
||||
$oDateFormat = new DateTimeFormat($oDateTimeFormat->ToDateFormat());
|
||||
$writer->setDateFormat($oDateFormat->ToExcel());
|
||||
$writer->setAuthor(UserRights::GetUserFriendlyName());
|
||||
$aHeaderTypes = array();
|
||||
$aHeaderNames = array();
|
||||
foreach($aHeaders as $Header)
|
||||
{
|
||||
$aHeaderNames[] = $Header['label'];
|
||||
$aHeaderTypes[] = $Header['type'];
|
||||
}
|
||||
$writer->writeSheet($aData,'Sheet1', $aHeaderTypes, $aHeaderNames);
|
||||
$fExcelTime = microtime(true) - $fStartExcel;
|
||||
//$this->aStatistics['excel_build_duration'] = $fExcelTime;
|
||||
|
||||
$fTime = microtime(true);
|
||||
$data = $writer->writeToString();
|
||||
$fExcelSaveTime = microtime(true) - $fTime;
|
||||
//$this->aStatistics['excel_write_duration'] = $fExcelSaveTime;
|
||||
|
||||
@unlink($this->aStatusInfo['tmp_file']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function GetMimeType()
|
||||
{
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
}
|
||||
|
||||
public function GetFileExtension()
|
||||
{
|
||||
return 'xlsx';
|
||||
}
|
||||
|
||||
public function GetSupportedFormats()
|
||||
{
|
||||
return array('xlsx' => Dict::S('Core:BulkExport:XLSXFormat'));
|
||||
}
|
||||
}
|
||||
213
sources/Application/BulkExport/htmlbulkexport.class.inc.php
Normal file
213
sources/Application/BulkExport/htmlbulkexport.class.inc.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
// Copyright (C) 2024 Combodo SAS
|
||||
//
|
||||
// This file is part of iTop.
|
||||
//
|
||||
// iTop is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// iTop is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
use Combodo\iTop\Application\WebPage\Page;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
|
||||
/**
|
||||
* Bulk export: HTML export
|
||||
*
|
||||
* @copyright Copyright (C) 2024 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
class HTMLBulkExport extends TabularBulkExport
|
||||
{
|
||||
public function DisplayUsage(Page $oP)
|
||||
{
|
||||
$oP->p(" * html format options:");
|
||||
$oP->p(" *\tfields: (mandatory) the comma separated list of field codes to export (e.g: name,org_id,service_name...).");
|
||||
}
|
||||
|
||||
public function EnumFormParts()
|
||||
{
|
||||
return array_merge(parent::EnumFormParts(), array('interactive_fields_html' => array('interactive_fields_html')));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oP
|
||||
* @param $sPartId
|
||||
*
|
||||
* @return UIContentBlock
|
||||
*/
|
||||
public function GetFormPart(WebPage $oP, $sPartId)
|
||||
{
|
||||
switch ($sPartId) {
|
||||
case 'interactive_fields_html':
|
||||
return $this->GetInteractiveFieldsWidget($oP, 'interactive_fields_html');
|
||||
break;
|
||||
|
||||
default:
|
||||
return parent:: GetFormPart($oP, $sPartId);
|
||||
}
|
||||
}
|
||||
|
||||
protected function GetSampleData($oObj, $sAttCode)
|
||||
{
|
||||
if ($sAttCode != 'id')
|
||||
{
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode);
|
||||
if ($oAttDef instanceof AttributeDateTime) // AttributeDate is derived from AttributeDateTime
|
||||
{
|
||||
$sClass = (get_class($oAttDef) == 'AttributeDateTime') ? 'user-formatted-date-time' : 'user-formatted-date';
|
||||
|
||||
return '<div class="'.$sClass.'" data-date="'.$oObj->Get($sAttCode).'">'.utils::EscapeHtml($oAttDef->GetEditValue($oObj->Get($sAttCode), $oObj)).'</div>';
|
||||
}
|
||||
}
|
||||
return $this->GetValue($oObj, $sAttCode);
|
||||
}
|
||||
|
||||
protected function GetValue($oObj, $sAttCode)
|
||||
{
|
||||
switch($sAttCode)
|
||||
{
|
||||
case 'id':
|
||||
$sRet = $oObj->GetHyperlink();
|
||||
break;
|
||||
|
||||
default:
|
||||
$value = $oObj->Get($sAttCode);
|
||||
if ($value instanceof ormCaseLog)
|
||||
{
|
||||
$sRet = $value->GetAsSimpleHtml();
|
||||
}
|
||||
elseif ($value instanceof ormStopWatch)
|
||||
{
|
||||
$sRet = $value->GetTimeSpent();
|
||||
}
|
||||
else
|
||||
{
|
||||
$sRet = $oObj->GetAsHtml($sAttCode);
|
||||
}
|
||||
}
|
||||
return $sRet;
|
||||
}
|
||||
|
||||
public function GetHeader()
|
||||
{
|
||||
$sData = '';
|
||||
|
||||
$oSet = new DBObjectSet($this->oSearch);
|
||||
$this->aStatusInfo['status'] = 'running';
|
||||
$this->aStatusInfo['position'] = 0;
|
||||
$this->aStatusInfo['total'] = $oSet->Count();
|
||||
|
||||
$sData .= "<table class=\"listResults\">\n";
|
||||
$sData .= "<thead>\n";
|
||||
$sData .= "<tr>\n";
|
||||
foreach($this->aStatusInfo['fields'] as $iCol => $aFieldSpec)
|
||||
{
|
||||
$sData .= "<th>".$aFieldSpec['sColLabel']."</th>\n";
|
||||
}
|
||||
$sData .= "</tr>\n";
|
||||
$sData .= "</thead>\n";
|
||||
$sData .= "<tbody>\n";
|
||||
return $sData;
|
||||
}
|
||||
|
||||
public function GetNextChunk(&$aStatus)
|
||||
{
|
||||
$sRetCode = 'run';
|
||||
$iPercentage = 0;
|
||||
|
||||
$oSet = new DBObjectSet($this->oSearch);
|
||||
$oSet->SetLimit($this->iChunkSize, $this->aStatusInfo['position']);
|
||||
$this->OptimizeColumnLoad($oSet);
|
||||
|
||||
$sFirstAlias = $this->oSearch->GetClassAlias();
|
||||
$sClass = $this->oSearch->GetClass();
|
||||
|
||||
$iCount = 0;
|
||||
$sData = '';
|
||||
$iPreviousTimeLimit = ini_get('max_execution_time');
|
||||
$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
|
||||
while($aRow = $oSet->FetchAssoc())
|
||||
{
|
||||
set_time_limit(intval($iLoopTimeLimit));
|
||||
$oMainObj = $aRow[$sFirstAlias];
|
||||
$sHilightClass = '';
|
||||
if ($oMainObj)
|
||||
{
|
||||
$sHilightClass = MetaModel::GetHilightClass($sClass, $aRow[$sFirstAlias]);
|
||||
}
|
||||
if ($sHilightClass != '')
|
||||
{
|
||||
$sData .= "<tr class=\"$sHilightClass\">";
|
||||
}
|
||||
else
|
||||
{
|
||||
$sData .= "<tr>";
|
||||
}
|
||||
foreach($this->aStatusInfo['fields'] as $iCol => $aFieldSpec)
|
||||
{
|
||||
$sAlias = $aFieldSpec['sAlias'];
|
||||
$sAttCode = $aFieldSpec['sAttCode'];
|
||||
|
||||
$oObj = $aRow[$sAlias];
|
||||
$sField = '';
|
||||
if ($oObj)
|
||||
{
|
||||
$sField = $this->GetValue($oObj, $sAttCode);
|
||||
}
|
||||
$sValue = ($sField === '') ? ' ' : $sField;
|
||||
$sData .= "<td>$sValue</td>";
|
||||
}
|
||||
$sData .= "</tr>";
|
||||
$iCount++;
|
||||
}
|
||||
set_time_limit(intval($iPreviousTimeLimit));
|
||||
$this->aStatusInfo['position'] += $this->iChunkSize;
|
||||
if ($this->aStatusInfo['total'] == 0)
|
||||
{
|
||||
$iPercentage = 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
$iPercentage = floor(min(100.0, 100.0*$this->aStatusInfo['position']/$this->aStatusInfo['total']));
|
||||
}
|
||||
|
||||
if ($iCount < $this->iChunkSize)
|
||||
{
|
||||
$sRetCode = 'done';
|
||||
}
|
||||
|
||||
$aStatus = array('code' => $sRetCode, 'message' => Dict::S('Core:BulkExport:RetrievingData'), 'percentage' => $iPercentage);
|
||||
return $sData;
|
||||
}
|
||||
|
||||
public function GetFooter()
|
||||
{
|
||||
$sData = "</tbody>\n";
|
||||
$sData .= "</table>\n";
|
||||
return $sData;
|
||||
}
|
||||
|
||||
public function GetSupportedFormats()
|
||||
{
|
||||
return array('html' => Dict::S('Core:BulkExport:HTMLFormat'));
|
||||
}
|
||||
|
||||
public function GetMimeType()
|
||||
{
|
||||
return 'text/html';
|
||||
}
|
||||
|
||||
public function GetFileExtension()
|
||||
{
|
||||
return 'html';
|
||||
}
|
||||
}
|
||||
349
sources/Application/BulkExport/pdfbulkexport.class.inc.php
Normal file
349
sources/Application/BulkExport/pdfbulkexport.class.inc.php
Normal file
@@ -0,0 +1,349 @@
|
||||
<?php
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\Component\FieldSet\FieldSetUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectOptionUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\MultiColumn\Column\ColumnUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\MultiColumn\MultiColumnUIBlockFactory;
|
||||
use Combodo\iTop\Application\WebPage\Page;
|
||||
use Combodo\iTop\Application\WebPage\PDFPage;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
|
||||
/**
|
||||
* Bulk export: PDF export, based on the HTML export converted to PDF
|
||||
*
|
||||
* @copyright Copyright (C) 2024 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
class PDFBulkExport extends HTMLBulkExport
|
||||
{
|
||||
/**
|
||||
* @var string For sample purposes
|
||||
* @internal
|
||||
* @since 2.7.8
|
||||
*/
|
||||
const ENUM_OUTPUT_TYPE_SAMPLE = 'sample';
|
||||
/**
|
||||
* @var string For the real export
|
||||
* @internal
|
||||
* @since 2.7.8
|
||||
*/
|
||||
const ENUM_OUTPUT_TYPE_REAL = 'real';
|
||||
|
||||
public function DisplayUsage(Page $oP)
|
||||
{
|
||||
$oP->p(" * pdf format options:");
|
||||
$oP->p(" *\tfields: (mandatory) the comma separated list of field codes to export (e.g: name,org_id,service_name...).");
|
||||
$oP->p(" *\tpage_size: (optional) size of the page. One of A4, A3, Letter (default is 'A4').");
|
||||
$oP->p(" *\tpage_orientation: (optional) the orientation of the page. Either Portrait or Landscape (default is 'Portrait').");
|
||||
$oP->p(" *\tdate_format: the format to use when exporting date and time fields (default = the SQL format). e.g. 'Y-m-d H:i:s'");
|
||||
}
|
||||
|
||||
public function EnumFormParts()
|
||||
{
|
||||
return array_merge(array('pdf_options' => array('pdf_options')), parent::EnumFormParts());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oP
|
||||
* @param $sPartId
|
||||
*
|
||||
* @return UIContentBlock
|
||||
*/
|
||||
public function GetFormPart(WebPage $oP, $sPartId)
|
||||
{
|
||||
switch ($sPartId) {
|
||||
case 'pdf_options':
|
||||
$oPanel = PanelUIBlockFactory::MakeNeutral(Dict::S('Core:BulkExport:PDFOptions'));
|
||||
|
||||
$oMulticolumn = MultiColumnUIBlockFactory::MakeStandard();
|
||||
$oPanel->AddSubBlock($oMulticolumn);
|
||||
|
||||
$oFieldSetFormat = FieldSetUIBlockFactory::MakeStandard(Dict::S('Core:BulkExport:PDFPageFormat'));
|
||||
$oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oFieldSetFormat));
|
||||
|
||||
//page format
|
||||
$oSelectFormat = SelectUIBlockFactory::MakeForSelectWithLabel("page_size", Dict::S('Core:BulkExport:PDFPageSize'));
|
||||
$oFieldSetFormat->AddSubBlock($oSelectFormat);
|
||||
|
||||
$aPossibleFormat = ['A3', 'A4', 'Letter'];
|
||||
$sDefaultFormat = 'A4';
|
||||
foreach ($aPossibleFormat as $sVal) {
|
||||
$oSelectFormat->AddSubBlock(SelectOptionUIBlockFactory::MakeForSelectOption($sVal, utils::EscapeHtml(Dict::S('Core:BulkExport:PageSize-'.$sVal)), ($sVal == $sDefaultFormat)));
|
||||
}
|
||||
$oFieldSetFormat->AddSubBlock(new Html('</br>'));
|
||||
|
||||
$oSelectOrientation = SelectUIBlockFactory::MakeForSelectWithLabel("page_orientation", Dict::S('Core:BulkExport:PDFPageOrientation'));
|
||||
$oFieldSetFormat->AddSubBlock($oSelectOrientation);
|
||||
|
||||
$aPossibleOrientation = ['P', 'L'];
|
||||
$sDefaultOrientation = 'L';
|
||||
foreach ($aPossibleOrientation as $sVal) {
|
||||
$oSelectOrientation->AddSubBlock(SelectOptionUIBlockFactory::MakeForSelectOption($sVal, utils::EscapeHtml(Dict::S('Core:BulkExport:PageOrientation-'.$sVal)), ($sVal == $sDefaultOrientation)));
|
||||
}
|
||||
|
||||
//date format
|
||||
$oFieldSetDate = FieldSetUIBlockFactory::MakeStandard(Dict::S('Core:BulkExport:DateTimeFormat'));
|
||||
$oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oFieldSetDate));
|
||||
|
||||
$sDateTimeFormat = utils::ReadParam('date_format', (string)AttributeDateTime::GetFormat(), true, 'raw_data');
|
||||
|
||||
$sDefaultFormat = utils::EscapeHtml((string)AttributeDateTime::GetFormat());
|
||||
$sExample = utils::EscapeHtml(date((string)AttributeDateTime::GetFormat()));
|
||||
$oRadioDefault = InputUIBlockFactory::MakeForInputWithLabel(Dict::Format('Core:BulkExport:DateTimeFormatDefault_Example', $sDefaultFormat, $sExample), "pdf_date_format_radio", "default", "pdf_date_time_format_default", "radio");
|
||||
$oRadioDefault->GetInput()->SetIsChecked(($sDateTimeFormat == (string)AttributeDateTime::GetFormat()));
|
||||
$oRadioDefault->SetBeforeInput(false);
|
||||
$oRadioDefault->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||
$oFieldSetDate->AddSubBlock($oRadioDefault);
|
||||
$oFieldSetDate->AddSubBlock(new Html('</br>'));
|
||||
|
||||
$sFormatInput = '<input type="text" size="15" name="date_format" id="pdf_custom_date_time_format" title="" value="'.utils::EscapeHtml($sDateTimeFormat).'"/>';
|
||||
$oRadioCustom = InputUIBlockFactory::MakeForInputWithLabel(Dict::Format('Core:BulkExport:DateTimeFormatCustom_Format', $sFormatInput), "pdf_date_format_radio", "custom", "pdf_date_time_format_custom", "radio");
|
||||
$oRadioCustom->SetDescription(Dict::S('UI:CSVImport:CustomDateTimeFormatTooltip'));
|
||||
$oRadioCustom->GetInput()->SetIsChecked($sDateTimeFormat !== (string)AttributeDateTime::GetFormat());
|
||||
$oRadioCustom->SetBeforeInput(false);
|
||||
$oRadioCustom->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||
$oFieldSetDate->AddSubBlock($oRadioCustom);
|
||||
|
||||
$oP->add_ready_script(
|
||||
<<<EOF
|
||||
$('#form_part_pdf_options').on('preview_updated', function() { FormatDatesInPreview('pdf', 'html'); });
|
||||
$('#pdf_date_time_format_default').on('click', function() { FormatDatesInPreview('pdf', 'html'); });
|
||||
$('#pdf_date_time_format_custom').on('click', function() { FormatDatesInPreview('pdf', 'html'); });
|
||||
$('#pdf_custom_date_time_format').on('click', function() { $('#pdf_date_time_format_custom').prop('checked', true); FormatDatesInPreview('pdf', 'html'); }).on('keyup', function() { FormatDatesInPreview('pdf', 'html'); });
|
||||
EOF
|
||||
);
|
||||
|
||||
return $oPanel;
|
||||
break;
|
||||
|
||||
default:
|
||||
return parent:: GetFormPart($oP, $sPartId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function ReadParameters()
|
||||
{
|
||||
parent::ReadParameters();
|
||||
$this->aStatusInfo['page_size'] = utils::ReadParam('page_size', 'A4', true, 'raw_data');
|
||||
$this->aStatusInfo['page_orientation'] = utils::ReadParam('page_orientation', 'L', true);
|
||||
|
||||
$sDateFormatRadio = utils::ReadParam('pdf_date_format_radio', '');
|
||||
switch($sDateFormatRadio)
|
||||
{
|
||||
case 'default':
|
||||
// Export from the UI => format = same as is the UI
|
||||
$this->aStatusInfo['date_format'] = (string)AttributeDateTime::GetFormat();
|
||||
break;
|
||||
|
||||
case 'custom':
|
||||
// Custom format specified from the UI
|
||||
$this->aStatusInfo['date_format'] = utils::ReadParam('date_format', (string)AttributeDateTime::GetFormat(), true, 'raw_data');
|
||||
break;
|
||||
|
||||
default:
|
||||
// Export from the command line (or scripted) => default format is SQL, as in previous versions of iTop, unless specified otherwise
|
||||
$this->aStatusInfo['date_format'] = utils::ReadParam('date_format', (string)AttributeDateTime::GetSQLFormat(), true, 'raw_data');
|
||||
}
|
||||
}
|
||||
|
||||
public function GetHeader()
|
||||
{
|
||||
$this->aStatusInfo['tmp_file'] = $this->MakeTmpFile('data');
|
||||
$sData = parent::GetHeader();
|
||||
$hFile = @fopen($this->aStatusInfo['tmp_file'], 'ab');
|
||||
if ($hFile === false)
|
||||
{
|
||||
throw new Exception('PDFBulkExport: Failed to open temporary data file: "'.$this->aStatusInfo['tmp_file'].'" for writing.');
|
||||
}
|
||||
fwrite($hFile, $sData."\n");
|
||||
fclose($hFile);
|
||||
return '';
|
||||
}
|
||||
|
||||
public function GetNextChunk(&$aStatus)
|
||||
{
|
||||
$oPrevFormat = AttributeDateTime::GetFormat();
|
||||
$oPrevDateFormat = AttributeDate::GetFormat();
|
||||
$oDateTimeFormat = new DateTimeFormat($this->aStatusInfo['date_format']);
|
||||
AttributeDateTime::SetFormat($oDateTimeFormat);
|
||||
AttributeDate::SetFormat(new DateTimeFormat($oDateTimeFormat->ToDateFormat()));
|
||||
$sData = parent::GetNextChunk($aStatus);
|
||||
AttributeDateTime::SetFormat($oPrevFormat);
|
||||
AttributeDate::SetFormat($oPrevDateFormat);
|
||||
$hFile = @fopen($this->aStatusInfo['tmp_file'], 'ab');
|
||||
if ($hFile === false)
|
||||
{
|
||||
throw new Exception('PDFBulkExport: Failed to open temporary data file: "'.$this->aStatusInfo['tmp_file'].'" for writing.');
|
||||
}
|
||||
fwrite($hFile, $sData."\n");
|
||||
fclose($hFile);
|
||||
return '';
|
||||
}
|
||||
|
||||
public function GetFooter()
|
||||
{
|
||||
$sData = parent::GetFooter();
|
||||
|
||||
// We need a lot of time for the PDF conversion
|
||||
set_time_limit(60 * 10); // 10 minutes max ???
|
||||
|
||||
$oPage = new PDFPage(Dict::Format('Core:BulkExportOf_Class', MetaModel::GetName($this->oSearch->GetClass())), $this->aStatusInfo['page_size'], $this->aStatusInfo['page_orientation']);
|
||||
$oPDF = $oPage->get_tcpdf();
|
||||
$oPDF->SetFontSize(8);
|
||||
|
||||
$oPage->add(file_get_contents($this->aStatusInfo['tmp_file']));
|
||||
$oPage->add($sData);
|
||||
|
||||
$sPDF = $oPage->get_pdf();
|
||||
|
||||
return $sPDF;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @since 2.7.8
|
||||
*/
|
||||
protected function GetSampleData($oObj, $sAttCode)
|
||||
{
|
||||
if ($sAttCode !== 'id')
|
||||
{
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode);
|
||||
|
||||
// As sample data will be displayed in the web browser, AttributeImage needs to be rendered with a regular HTML format, meaning its "src" looking like "data:image/png;base64,iVBORw0KGgoAAAANSUh..."
|
||||
// Whereas for the PDF generation it needs to be rendered with a TCPPDF-compatible format, meaning its "src" looking like "@iVBORw0KGgoAAAANSUh..."
|
||||
if ($oAttDef instanceof AttributeImage) {
|
||||
return $this->GetAttributeImageValue($oObj, $sAttCode, static::ENUM_OUTPUT_TYPE_SAMPLE);
|
||||
}
|
||||
}
|
||||
return parent::GetSampleData($oObj, $sAttCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DBObject $oObj
|
||||
* @param string $sAttCode
|
||||
*
|
||||
* @return int|string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function GetValue($oObj, $sAttCode)
|
||||
{
|
||||
switch ($sAttCode) {
|
||||
case 'id':
|
||||
$sRet = parent::GetValue($oObj, $sAttCode);
|
||||
break;
|
||||
|
||||
default:
|
||||
$value = $oObj->Get($sAttCode);
|
||||
if ($value instanceof ormDocument) {
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode);
|
||||
if ($oAttDef instanceof AttributeImage)
|
||||
{
|
||||
$sRet = $this->GetAttributeImageValue($oObj, $sAttCode, static::ENUM_OUTPUT_TYPE_REAL);
|
||||
}
|
||||
else
|
||||
{
|
||||
$sRet = parent::GetValue($oObj, $sAttCode);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$sRet = parent::GetValue($oObj, $sAttCode);
|
||||
}
|
||||
}
|
||||
return $sRet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DBObject $oObj
|
||||
* @param string $sAttCode
|
||||
* @param string $sOutputType {@see \PDFBulkExport::ENUM_OUTPUT_TYPE_SAMPLE}, {@see \PDFBulkExport::ENUM_OUTPUT_TYPE_REAL}
|
||||
*
|
||||
* @return string Rendered value of $oAttDef / $oValue according to the desired $sOutputType
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
*
|
||||
* @since 2.7.8 N°2244 method creation
|
||||
* @since 2.7.9 N°5588 signature change to get the object so that we can log all the needed information
|
||||
*/
|
||||
protected function GetAttributeImageValue(DBObject $oObj, string $sAttCode, string $sOutputType)
|
||||
{
|
||||
$oValue = $oObj->Get($sAttCode);
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode);
|
||||
|
||||
// To limit the image size in the PDF output, we have to enforce the size as height/width because max-width/max-height have no effect
|
||||
//
|
||||
$iDefaultMaxWidthPx = 48;
|
||||
$iDefaultMaxHeightPx = 48;
|
||||
if ($oValue->IsEmpty()) {
|
||||
$iNewWidth = $iDefaultMaxWidthPx;
|
||||
$iNewHeight = $iDefaultMaxHeightPx;
|
||||
|
||||
$sUrl = $oAttDef->Get('default_image');
|
||||
} else {
|
||||
$iMaxWidthPx = min($iDefaultMaxWidthPx, $oAttDef->Get('display_max_width'));
|
||||
$iMaxHeightPx = min($iDefaultMaxHeightPx, $oAttDef->Get('display_max_height'));
|
||||
|
||||
list($iWidth, $iHeight) = utils::GetImageSize($oValue->GetData());
|
||||
if ((is_null($iWidth)) || (is_null($iHeight)) || ($iWidth === 0) || ($iHeight === 0)) {
|
||||
// Avoid division by zero exception (SVGs, corrupted images, ...)
|
||||
$iNewWidth = $iDefaultMaxWidthPx;
|
||||
$iNewHeight = $iDefaultMaxHeightPx;
|
||||
|
||||
$sAttCode = $oAttDef->GetCode();
|
||||
IssueLog::Warning('AttributeImage: Cannot read image size', LogChannels::EXPORT, [
|
||||
'ObjClass' => get_class($oObj),
|
||||
'ObjKey' => $oObj->GetKey(),
|
||||
'ObjFriendlyName' => $oObj->GetName(),
|
||||
'AttCode' => $sAttCode,
|
||||
]);
|
||||
} else {
|
||||
$fScale = min($iMaxWidthPx / $iWidth, $iMaxHeightPx / $iHeight);
|
||||
$iNewWidth = $iWidth * $fScale;
|
||||
$iNewHeight = $iHeight * $fScale;
|
||||
}
|
||||
|
||||
$sValueAsBase64 = base64_encode($oValue->GetData());
|
||||
switch ($sOutputType) {
|
||||
case static::ENUM_OUTPUT_TYPE_SAMPLE:
|
||||
$sUrl = 'data:'.$oValue->GetMimeType().';base64,'.$sValueAsBase64;
|
||||
break;
|
||||
|
||||
case static::ENUM_OUTPUT_TYPE_REAL:
|
||||
default:
|
||||
// TCPDF requires base64-encoded images to be rendered without the usual "data:<MIMETYPE>;base64" header but with an "@"
|
||||
// @link https://tcpdf.org/examples/example_009/
|
||||
$sUrl = '@'.$sValueAsBase64;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$sRet = ($sUrl !== null) ? '<img src="'.$sUrl.'" style="width: '.$iNewWidth.'px; height: '.$iNewHeight.'px;">' : '';
|
||||
$sRet = '<div class="ibo-input-image--image-view">'.$sRet.'</div>';
|
||||
|
||||
return $sRet;
|
||||
}
|
||||
|
||||
public function GetSupportedFormats()
|
||||
{
|
||||
return array('pdf' => Dict::S('Core:BulkExport:PDFFormat'));
|
||||
}
|
||||
|
||||
public function GetMimeType()
|
||||
{
|
||||
return 'application/x-pdf';
|
||||
}
|
||||
|
||||
public function GetFileExtension()
|
||||
{
|
||||
return 'pdf';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
<?php
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\Component\FieldSet\FieldSetUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\MultiColumn\Column\ColumnUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\MultiColumn\MultiColumnUIBlockFactory;
|
||||
use Combodo\iTop\Application\WebPage\Page;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
|
||||
/**
|
||||
* Bulk export: "spreadsheet" export: a simplified HTML export in which the date/time columns are split in two column: date AND time
|
||||
*
|
||||
* @copyright Copyright (C) 2024 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
class SpreadsheetBulkExport extends TabularBulkExport
|
||||
{
|
||||
public function DisplayUsage(Page $oP)
|
||||
{
|
||||
$oP->p(" * spreadsheet format options:");
|
||||
$oP->p(" *\tfields: (mandatory) the comma separated list of field codes to export (e.g: name,org_id,service_name...).");
|
||||
$oP->p(" *\tno_localize: (optional) pass 1 to retrieve the raw (untranslated) values for enumerated fields. Default: 0.");
|
||||
$oP->p(" *\tdate_format: the format to use when exporting date and time fields (default = the SQL format). e.g. 'Y-m-d H:i:s'");
|
||||
$oP->p(" *\tformatted_text: set to 1 to formatted text fields with their HTML markup, 0 to remove formatting. Default is 1 (= formatted text)");
|
||||
}
|
||||
|
||||
public function EnumFormParts()
|
||||
{
|
||||
return array_merge(parent::EnumFormParts(), array('spreadsheet_options' => array('no-localize'), 'interactive_fields_spreadsheet' => array('interactive_fields_spreadsheet')));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oP
|
||||
* @param $sPartId
|
||||
*
|
||||
* @return UIContentBlock
|
||||
*/
|
||||
public function GetFormPart(WebPage $oP, $sPartId)
|
||||
{
|
||||
switch ($sPartId) {
|
||||
case 'interactive_fields_spreadsheet':
|
||||
return $this->GetInteractiveFieldsWidget($oP, 'interactive_fields_spreadsheet');
|
||||
break;
|
||||
|
||||
case 'spreadsheet_options':
|
||||
$oPanel = PanelUIBlockFactory::MakeNeutral(Dict::S('Core:BulkExport:SpreadsheetOptions'));
|
||||
|
||||
$oMulticolumn = MultiColumnUIBlockFactory::MakeStandard();
|
||||
$oPanel->AddSubBlock($oMulticolumn);
|
||||
|
||||
$oFieldSetFormat = FieldSetUIBlockFactory::MakeStandard(Dict::S('Core:BulkExport:TextFormat'));
|
||||
$oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oFieldSetFormat));
|
||||
|
||||
$oCheckBox = InputUIBlockFactory::MakeForInputWithLabel(Dict::S('Core:BulkExport:OptionFormattedText'), "formatted_text", "1", "spreadsheet_formatted_text", "checkbox");
|
||||
$oCheckBox->GetInput()->SetIsChecked((utils::ReadParam('formatted_text', 0) == 1));
|
||||
$oCheckBox->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||
$oCheckBox->SetBeforeInput(false);
|
||||
$oFieldSetFormat->AddSubBlock($oCheckBox);
|
||||
$oFieldSetFormat->AddSubBlock(new Html('<br>'));
|
||||
|
||||
$oCheckBox = InputUIBlockFactory::MakeForInputWithLabel(Dict::S('Core:BulkExport:OptionNoLocalize'), "no_localize", "1", "spreadsheet_no_localize", "checkbox");
|
||||
$oCheckBox->GetInput()->SetIsChecked((utils::ReadParam('no_localize', 0) == 1));
|
||||
$oCheckBox->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||
$oCheckBox->SetBeforeInput(false);
|
||||
$oFieldSetFormat->AddSubBlock($oCheckBox);
|
||||
|
||||
$oFieldSetDate = FieldSetUIBlockFactory::MakeStandard(Dict::S('Core:BulkExport:DateTimeFormat'));
|
||||
$oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oFieldSetDate));
|
||||
|
||||
$sDateTimeFormat = utils::ReadParam('date_format', (string)AttributeDateTime::GetFormat(), true, 'raw_data');
|
||||
|
||||
$sDefaultFormat = utils::EscapeHtml((string)AttributeDateTime::GetFormat());
|
||||
$sExample = utils::EscapeHtml(date((string)AttributeDateTime::GetFormat()));
|
||||
$oRadioDefault = InputUIBlockFactory::MakeForInputWithLabel(Dict::Format('Core:BulkExport:DateTimeFormatDefault_Example', $sDefaultFormat, $sExample), "spreadsheet_date_format_radio", "default", "spreadsheet_date_time_format_default", "radio");
|
||||
$oRadioDefault->GetInput()->SetIsChecked(($sDateTimeFormat == (string)AttributeDateTime::GetFormat()));
|
||||
$oRadioDefault->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||
$oRadioDefault->SetBeforeInput(false);
|
||||
$oFieldSetDate->AddSubBlock($oRadioDefault);
|
||||
$oFieldSetDate->AddSubBlock(new Html('</br>'));
|
||||
|
||||
$sFormatInput = '<input type="text" size="15" name="date_format" id="spreadsheet_custom_date_time_format" title="" value="'.utils::EscapeHtml($sDateTimeFormat).'"/>';
|
||||
$oRadioCustom = InputUIBlockFactory::MakeForInputWithLabel(Dict::Format('Core:BulkExport:DateTimeFormatCustom_Format', $sFormatInput), "spreadsheet_date_format_radio", "custom", "spreadsheet_date_time_format_custom", "radio");
|
||||
$oRadioCustom->SetDescription(Dict::S('UI:CSVImport:CustomDateTimeFormatTooltip'));
|
||||
$oRadioCustom->GetInput()->SetIsChecked($sDateTimeFormat !== (string)AttributeDateTime::GetFormat());
|
||||
$oRadioCustom->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||
$oRadioCustom->SetBeforeInput(false);
|
||||
$oFieldSetDate->AddSubBlock($oRadioCustom);
|
||||
|
||||
$oP->add_ready_script(
|
||||
<<<EOF
|
||||
$('#form_part_spreadsheet_options').on('preview_updated', function() { FormatDatesInPreview('spreadsheet', 'spreadsheet'); });
|
||||
$('#spreadsheet_date_time_format_default').on('click', function() { FormatDatesInPreview('spreadsheet', 'spreadsheet'); });
|
||||
$('#spreadsheet_date_time_format_custom').on('click', function() { FormatDatesInPreview('spreadsheet', 'spreadsheet'); });
|
||||
$('#spreadsheet_custom_date_time_format').on('click', function() { $('#spreadsheet_date_time_format_custom').prop('checked', true); });
|
||||
$('#spreadsheet_custom_date_time_format').on('click', function() { $('#spreadsheet_date_time_format_custom').prop('checked', true); FormatDatesInPreview('spreadsheet', 'spreadsheet'); }).on('keyup', function() { FormatDatesInPreview('spreadsheet', 'spreadsheet'); });
|
||||
EOF
|
||||
);
|
||||
|
||||
return $oPanel;
|
||||
break;
|
||||
|
||||
default:
|
||||
return parent:: GetFormPart($oP, $sPartId);
|
||||
}
|
||||
}
|
||||
|
||||
public function ReadParameters()
|
||||
{
|
||||
parent::ReadParameters();
|
||||
$this->aStatusInfo['formatted_text'] = (bool)utils::ReadParam('formatted_text', 1, true);
|
||||
|
||||
$sDateFormatRadio = utils::ReadParam('spreadsheet_date_format_radio', '');
|
||||
switch($sDateFormatRadio)
|
||||
{
|
||||
case 'default':
|
||||
// Export from the UI => format = same as is the UI
|
||||
$this->aStatusInfo['date_format'] = (string)AttributeDateTime::GetFormat();
|
||||
break;
|
||||
|
||||
case 'custom':
|
||||
// Custom format specified from the UI
|
||||
$this->aStatusInfo['date_format'] = utils::ReadParam('date_format', (string)AttributeDateTime::GetFormat(), true, 'raw_data');
|
||||
break;
|
||||
|
||||
default:
|
||||
// Export from the command line (or scripted) => default format is SQL, as in previous versions of iTop, unless specified otherwise
|
||||
$this->aStatusInfo['date_format'] = utils::ReadParam('date_format', (string)AttributeDateTime::GetSQLFormat(), true, 'raw_data');
|
||||
}
|
||||
}
|
||||
|
||||
protected function GetSampleData($oObj, $sAttCode)
|
||||
{
|
||||
if ($sAttCode != 'id')
|
||||
{
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode);
|
||||
if ($oAttDef instanceof AttributeDateTime) // AttributeDate is derived from AttributeDateTime
|
||||
{
|
||||
$sClass = (get_class($oAttDef) == 'AttributeDateTime') ? 'user-formatted-date-time' : 'user-formatted-date';
|
||||
|
||||
return '<div class="'.$sClass.'" data-date="'.$oObj->Get($sAttCode).'">'.utils::EscapeHtml($oAttDef->GetEditValue($oObj->Get($sAttCode), $oObj)).'</div>';
|
||||
}
|
||||
}
|
||||
return $this->GetValue($oObj, $sAttCode);
|
||||
}
|
||||
|
||||
protected function GetValue($oObj, $sAttCode)
|
||||
{
|
||||
$bFormattedText = (array_key_exists('formatted_text', $this->aStatusInfo) ? $this->aStatusInfo['formatted_text'] : false);
|
||||
switch($sAttCode)
|
||||
{
|
||||
case 'id':
|
||||
$sRet = $oObj->GetKey();
|
||||
break;
|
||||
|
||||
default:
|
||||
$value = $oObj->Get($sAttCode);
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode);
|
||||
if ($value instanceof ormCaseLog) {
|
||||
$sRet = str_replace("\n", "<br/>", utils::EscapeHtml($value->__toString()));
|
||||
} elseif ($value instanceof ormStopWatch) {
|
||||
$sRet = $value->GetTimeSpent();
|
||||
} elseif ($value instanceof ormDocument) {
|
||||
$sRet = '';
|
||||
} elseif ($oAttDef instanceof AttributeText)
|
||||
{
|
||||
if ($bFormattedText)
|
||||
{
|
||||
// Replace paragraphs (<p...>...</p>, etc) by line breaks (<br/>) since Excel (pre-2016) splits the cells when there is a paragraph
|
||||
$sRet = static::HtmlToSpreadsheet($oObj->GetAsHTML($sAttCode));
|
||||
}
|
||||
else
|
||||
{
|
||||
$sRet = utils::HtmlToText($oObj->GetAsHTML($sAttCode));
|
||||
}
|
||||
}
|
||||
elseif ($oAttDef instanceof AttributeString)
|
||||
{
|
||||
$sRet = $oObj->GetAsHTML($sAttCode);
|
||||
}
|
||||
elseif ($oAttDef instanceof AttributeCustomFields)
|
||||
{
|
||||
// Stick to the weird implementation made in GetNextChunk
|
||||
$sRet = utils::TextToHtml($oObj->GetEditValue($sAttCode));
|
||||
}
|
||||
else {
|
||||
if ($this->bLocalizeOutput) {
|
||||
$sRet = utils::EscapeHtml($oObj->GetEditValue());
|
||||
} else {
|
||||
$sRet = utils::EscapeHtml((string)$value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $sRet;
|
||||
}
|
||||
|
||||
public function SetHttpHeaders(WebPage $oPage)
|
||||
{
|
||||
// Integration within MS-Excel web queries + HTTPS + IIS:
|
||||
// MS-IIS set these header values with no-cache... while Excel fails to do the job if using HTTPS
|
||||
// Then the fix is to force the reset of header values Pragma and Cache-control
|
||||
$oPage->add_header("Pragma:");
|
||||
$oPage->add_header("Cache-control:");
|
||||
}
|
||||
|
||||
public function GetHeader()
|
||||
{
|
||||
$oSet = new DBObjectSet($this->oSearch);
|
||||
$this->aStatusInfo['status'] = 'running';
|
||||
$this->aStatusInfo['position'] = 0;
|
||||
$this->aStatusInfo['total'] = $oSet->Count();
|
||||
|
||||
$aData = array();
|
||||
foreach($this->aStatusInfo['fields'] as $iCol => $aFieldSpec)
|
||||
{
|
||||
$sColLabel = $aFieldSpec['sColLabel'];
|
||||
if ($aFieldSpec['sAttCode'] != 'id')
|
||||
{
|
||||
$oAttDef = MetaModel::GetAttributeDef($aFieldSpec['sClass'], $aFieldSpec['sAttCode']);
|
||||
$oFinalAttDef = $oAttDef->GetFinalAttDef();
|
||||
if (get_class($oFinalAttDef) == 'AttributeDateTime')
|
||||
{
|
||||
$aData[] = $sColLabel.' ('.Dict::S('UI:SplitDateTime-Date').')';
|
||||
$aData[] = $sColLabel.' ('.Dict::S('UI:SplitDateTime-Time').')';
|
||||
}
|
||||
else
|
||||
{
|
||||
$aData[] = $sColLabel;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$aData[] = $sColLabel;
|
||||
}
|
||||
}
|
||||
$sData = '';
|
||||
$sData .= "<table border=\"1\">\n";
|
||||
$sData .= "<tr>\n";
|
||||
foreach($aData as $sLabel)
|
||||
{
|
||||
$sData .= "<td>".$sLabel."</td>\n";
|
||||
}
|
||||
$sData .= "</tr>\n";
|
||||
return $sData;
|
||||
}
|
||||
|
||||
public function GetNextChunk(&$aStatus)
|
||||
{
|
||||
$sRetCode = 'run';
|
||||
$iPercentage = 0;
|
||||
|
||||
$oSet = new DBObjectSet($this->oSearch);
|
||||
$oSet->SetLimit($this->iChunkSize, $this->aStatusInfo['position']);
|
||||
$this->OptimizeColumnLoad($oSet);
|
||||
|
||||
$sExportDateTimeFormat = $this->aStatusInfo['date_format'];
|
||||
$bFormattedText = (array_key_exists('formatted_text', $this->aStatusInfo) ? $this->aStatusInfo['formatted_text'] : false);
|
||||
// Date & time formats
|
||||
$oDateTimeFormat = new DateTimeFormat($sExportDateTimeFormat);
|
||||
$oDateFormat = new DateTimeFormat($oDateTimeFormat->ToDateFormat());
|
||||
$oTimeFormat = new DateTimeFormat($oDateTimeFormat->ToTimeFormat());
|
||||
|
||||
$iCount = 0;
|
||||
$sData = '';
|
||||
$iPreviousTimeLimit = ini_get('max_execution_time');
|
||||
$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
|
||||
while($aRow = $oSet->FetchAssoc())
|
||||
{
|
||||
set_time_limit(intval($iLoopTimeLimit));
|
||||
|
||||
$sData .= "<tr>";
|
||||
foreach($this->aStatusInfo['fields'] as $iCol => $aFieldSpec)
|
||||
{
|
||||
$sAlias = $aFieldSpec['sAlias'];
|
||||
$sAttCode = $aFieldSpec['sAttCode'];
|
||||
|
||||
$sField = '';
|
||||
/** @var \DBObject $oObj */
|
||||
$oObj = $aRow[$sAlias];
|
||||
if ($oObj == null)
|
||||
{
|
||||
$sData .= "<td x:str></td>";
|
||||
continue;
|
||||
}
|
||||
|
||||
switch($sAttCode)
|
||||
{
|
||||
case 'id':
|
||||
$sField = $oObj->GetKey();
|
||||
$sData .= "<td>$sField</td>";
|
||||
break;
|
||||
|
||||
default:
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode);
|
||||
$oFinalAttDef = $oAttDef->GetFinalAttDef();
|
||||
if (get_class($oFinalAttDef) == 'AttributeDateTime')
|
||||
{
|
||||
// Split the date and time in two columns
|
||||
$sDate = $oDateFormat->Format($oObj->Get($sAttCode));
|
||||
$sTime = $oTimeFormat->Format($oObj->Get($sAttCode));
|
||||
$sData .= "<td>$sDate</td>";
|
||||
$sData .= "<td>$sTime</td>";
|
||||
}
|
||||
else if (get_class($oFinalAttDef) == 'AttributeDate') {
|
||||
$sDate = $oDateFormat->Format($oObj->Get($sAttCode));
|
||||
$sData .= "<td>$sDate</td>";
|
||||
} else if ($oAttDef instanceof AttributeCaseLog) {
|
||||
$rawValue = $oObj->Get($sAttCode);
|
||||
$sField = str_replace("\n", "<br/>", utils::EscapeHtml($rawValue->__toString()));
|
||||
// Trick for Excel: treat the content as text even if it begins with an equal sign
|
||||
$sData .= "<td x:str>$sField</td>";
|
||||
} elseif ($oAttDef instanceof AttributeText) {
|
||||
if ($bFormattedText) {
|
||||
// Replace paragraphs (<p...>...</p>, etc) by line breaks (<br/>) since Excel (pre-2016) splits the cells when there is a paragraph
|
||||
$sField = static::HtmlToSpreadsheet($oObj->GetAsHTML($sAttCode));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Convert to plain text
|
||||
$sField = utils::HtmlToText($oObj->GetAsHTML($sAttCode));
|
||||
}
|
||||
$sData .= "<td x:str>$sField</td>";
|
||||
}
|
||||
elseif ($oAttDef instanceof AttributeCustomFields)
|
||||
{
|
||||
// GetAsHTML returns a table that would not fit
|
||||
$sField = utils::TextToHtml($oObj->GetEditValue($sAttCode));
|
||||
$sData .= "<td x:str>$sField</td>";
|
||||
}
|
||||
else if ($oAttDef instanceof AttributeString)
|
||||
{
|
||||
$sField = $oObj->GetAsHTML($sAttCode, $this->bLocalizeOutput);
|
||||
$sData .= "<td x:str>$sField</td>";
|
||||
}
|
||||
else if ($oAttDef instanceof AttributeTagSet)
|
||||
{
|
||||
$sField = utils::HtmlEntities($oObj->GetAsCSV($sAttCode, $this->bLocalizeOutput, ''));
|
||||
$sData .= "<td x:str>$sField</td>";
|
||||
}
|
||||
else {
|
||||
$rawValue = $oObj->Get($sAttCode);
|
||||
if ($this->bLocalizeOutput) {
|
||||
$sField = utils::EscapeHtml($oFinalAttDef->GetEditValue($rawValue));
|
||||
} else {
|
||||
$sField = utils::EscapeHtml($rawValue);
|
||||
}
|
||||
$sData .= "<td>$sField</td>";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
$sData .= "</tr>";
|
||||
$iCount++;
|
||||
}
|
||||
set_time_limit(intval($iPreviousTimeLimit));
|
||||
$this->aStatusInfo['position'] += $this->iChunkSize;
|
||||
if ($this->aStatusInfo['total'] == 0)
|
||||
{
|
||||
$iPercentage = 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
$iPercentage = floor(min(100.0, 100.0*$this->aStatusInfo['position']/$this->aStatusInfo['total']));
|
||||
}
|
||||
|
||||
if ($iCount < $this->iChunkSize)
|
||||
{
|
||||
$sRetCode = 'done';
|
||||
}
|
||||
|
||||
$aStatus = array('code' => $sRetCode, 'message' => Dict::S('Core:BulkExport:RetrievingData'), 'percentage' => $iPercentage);
|
||||
return $sData;
|
||||
}
|
||||
|
||||
public function GetFooter()
|
||||
{
|
||||
$sData = "</table>\n";
|
||||
|
||||
return $sData;
|
||||
}
|
||||
|
||||
public function GetSupportedFormats()
|
||||
{
|
||||
return array('spreadsheet' => Dict::S('Core:BulkExport:SpreadsheetFormat'));
|
||||
}
|
||||
|
||||
public function GetMimeType()
|
||||
{
|
||||
return 'text/html';
|
||||
}
|
||||
|
||||
public function GetFileExtension()
|
||||
{
|
||||
return 'html';
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup all markup displayed as line breaks (except <br> tags) since this
|
||||
* causes Excel (pre-2016) to generate extra lines in the table, thus breaking
|
||||
* the tabular disposition of the export
|
||||
* Note: Excel 2016 also refuses line breaks, so the only solution for this case is alas plain text
|
||||
* @param string $sHtml The HTML to cleanup
|
||||
* @return string The cleaned HTML
|
||||
*/
|
||||
public static function HtmlToSpreadsheet($sHtml)
|
||||
{
|
||||
if (trim(strip_tags($sHtml)) === '')
|
||||
{
|
||||
// Display this value as an empty cell in the table
|
||||
return ' ';
|
||||
}
|
||||
// The tags listed here are a subset of the whitelist defined in HTMLDOMSanitizer
|
||||
// Tags causing a visual "line break" in the displayed page (i.e. display: block) are to be replaced by a <span> followed by a <br/>
|
||||
// in order to preserve any inline style/attribute of the removed tag
|
||||
$aTagsToReplace = array(
|
||||
'pre', 'div', 'p', 'hr', 'center', 'h1', 'h2', 'h3', 'h4', 'li', 'fieldset', 'legend', 'nav', 'section', 'tr', 'caption',
|
||||
);
|
||||
// Tags to completely remove from the markup
|
||||
$aTagsToRemove = array(
|
||||
'table', 'thead', 'tbody', 'ul', 'ol', 'td', 'th',
|
||||
);
|
||||
|
||||
// Remove the englobing <div class="HTML" >...</div> to prevent an extra line break
|
||||
$sHtml = preg_replace('|^<div class="HTML" >(.*)</div>$|s', '$1', $sHtml); // Must use the "s" (. matches newline) modifier
|
||||
|
||||
foreach($aTagsToReplace as $sTag)
|
||||
{
|
||||
$sHtml = preg_replace("|<{$sTag} ?([^>]*)>|is", '<span $1>', $sHtml);
|
||||
$sHtml = preg_replace("|</{$sTag}>|i", '</span><br/>', $sHtml);
|
||||
}
|
||||
|
||||
foreach($aTagsToRemove as $sTag)
|
||||
{
|
||||
$sHtml = preg_replace("|<{$sTag} ?([^>]*)>|is", '', $sHtml);
|
||||
$sHtml = preg_replace("|</{$sTag}>|i", '', $sHtml);
|
||||
}
|
||||
|
||||
// Remove any trailing <br/>, if any, to prevent an extra line break
|
||||
$sHtml = preg_replace("|<br/>$|", '', $sHtml);
|
||||
|
||||
return $sHtml;
|
||||
}
|
||||
}
|
||||
528
sources/Application/BulkExport/tabularbulkexport.class.inc.php
Normal file
528
sources/Application/BulkExport/tabularbulkexport.class.inc.php
Normal file
@@ -0,0 +1,528 @@
|
||||
<?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(), array('tabular_fields' => array('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 = array();
|
||||
// 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 = array();
|
||||
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] = array('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'] = array('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] = array('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] = array('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] = array('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 = array();
|
||||
foreach($aSelectedClasses as $sAlias => $sClassName)
|
||||
{
|
||||
if (UserRights::IsActionAllowed($sClassName, UR_ACTION_BULK_READ, $oSet) != UR_ALLOWED_NO)
|
||||
{
|
||||
$aAuthorizedClasses[$sAlias] = $sClassName;
|
||||
}
|
||||
}
|
||||
$aAllFieldsByAlias = array();
|
||||
$aAllAttCodes = array();
|
||||
foreach($aAuthorizedClasses as $sAlias => $sClass)
|
||||
{
|
||||
$aAllFields = array();
|
||||
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 = array(
|
||||
array('attcodeex' => 'id', 'code' => $sShortAlias.'id', 'unique_label' => $sShortAlias.Dict::S('UI:CSVImport:idField'), 'label' => $sShortAlias.'id'),
|
||||
array('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 = array();
|
||||
}
|
||||
$aAllFields[] = array('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 = array();
|
||||
foreach($aSubAttr as $aSubAttDef)
|
||||
{
|
||||
$aValidSubAttr[] = array('attcodeex' => $aSubAttDef['code'], 'code' => $sShortAlias.$aSubAttDef['code'], 'label' => $aSubAttDef['label'], 'unique_label' => $sShortAlias.$aSubAttDef['unique_label']);
|
||||
}
|
||||
$aAllFields[] = array('attcodeex' => $sAttCode, 'code' => $sShortAlias.$sAttCode, 'label' => $sShortLabel, 'unique_label' => $sLabel, 'subattr' => $aValidSubAttr);
|
||||
}
|
||||
}
|
||||
usort($aAllFields, array(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] = array();
|
||||
}
|
||||
$oSet->OptimizeColumnLoad($aColumns);
|
||||
$iPreviewLimit = 3;
|
||||
$oSet->SetLimit($iPreviewLimit);
|
||||
$aSampleData = array();
|
||||
while($aRow = $oSet->FetchAssoc())
|
||||
{
|
||||
$aSampleRow = array();
|
||||
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 = array(
|
||||
'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;
|
||||
}
|
||||
|
||||
static public 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', array('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 = array();
|
||||
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'] = array();
|
||||
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'][] = array(
|
||||
'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 = array();
|
||||
|
||||
foreach($this->aStatusInfo['fields'] as $iCol => $aFieldSpec)
|
||||
{
|
||||
$sClass = $aFieldSpec['sClass'];
|
||||
$sAlias = $aFieldSpec['sAlias'];
|
||||
$sAttCode = $aFieldSpec['sAttCode'];
|
||||
|
||||
if (!array_key_exists($sAlias, $aColumnsToLoad))
|
||||
{
|
||||
$aColumnsToLoad[$sAlias] = array();
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
232
sources/Application/BulkExport/xmlbulkexport.class.inc.php
Normal file
232
sources/Application/BulkExport/xmlbulkexport.class.inc.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?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\Component\Panel\PanelUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\MultiColumn\Column\ColumnUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\MultiColumn\MultiColumnUIBlockFactory;
|
||||
use Combodo\iTop\Application\WebPage\Page;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
|
||||
/**
|
||||
* Bulk export: XML export
|
||||
*
|
||||
* @copyright Copyright (C) 2015-2024 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
class XMLBulkExport extends BulkExport
|
||||
{
|
||||
public function DisplayUsage(Page $oP)
|
||||
{
|
||||
$oP->p(" * xml format options:");
|
||||
$oP->p(" *\tno_localize: set to 1 to retrieve non-localized values (for instance for ENUM values). Default is 0 (= localized values)");
|
||||
$oP->p(" *\tlinksets: set to 1 to retrieve links to related objects (1-N or N-N relations). Default is 0 (= only scalar fields)");
|
||||
}
|
||||
|
||||
public function EnumFormParts()
|
||||
{
|
||||
return array_merge(parent::EnumFormParts(), array('xml_options' => array('xml_no_options')));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oP
|
||||
* @param $sPartId
|
||||
*
|
||||
* @return UIContentBlock
|
||||
*/
|
||||
public function GetFormPart(WebPage $oP, $sPartId)
|
||||
{
|
||||
switch ($sPartId) {
|
||||
case 'xml_options':
|
||||
|
||||
$oPanel = PanelUIBlockFactory::MakeNeutral(Dict::S('Core:BulkExport:XMLOptions'));
|
||||
|
||||
$oMulticolumn = MultiColumnUIBlockFactory::MakeStandard();
|
||||
$oPanel->AddSubBlock($oMulticolumn);
|
||||
|
||||
$oCheckBoxLocalize = InputUIBlockFactory::MakeForInputWithLabel(Dict::S('Core:BulkExport:OptionNoLocalize'), "no_localize", "1", "xml_no_localize", "checkbox");
|
||||
$oCheckBoxLocalize->GetInput()->SetIsChecked((utils::ReadParam('no_localize', 0) == 1));
|
||||
$oCheckBoxLocalize->SetBeforeInput(false);
|
||||
$oCheckBoxLocalize->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||
$oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxLocalize));
|
||||
|
||||
$oCheckBoxLink = InputUIBlockFactory::MakeForInputWithLabel(Dict::S('Core:BulkExport:OptionLinkSets'), "linksets", "1", "xml_linksets", "checkbox");
|
||||
$oCheckBoxLink->GetInput()->SetIsChecked((utils::ReadParam('linksets', 0) == 1));
|
||||
$oCheckBoxLink->SetBeforeInput(false);
|
||||
$oCheckBoxLink->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||
$oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxLink));
|
||||
|
||||
return $oPanel;
|
||||
break;
|
||||
|
||||
default:
|
||||
return parent:: GetFormPart($oP, $sPartId);
|
||||
}
|
||||
}
|
||||
|
||||
public function ReadParameters()
|
||||
{
|
||||
parent::ReadParameters();
|
||||
|
||||
$this->aStatusInfo['linksets'] = (utils::ReadParam('linksets', 0) == 1);
|
||||
}
|
||||
|
||||
protected function GetSampleData($oObj, $sAttCode)
|
||||
{
|
||||
$sRet = ($sAttCode == 'id') ? $oObj->GetKey() : $oObj->GetAsXML($sAttCode);
|
||||
return $sRet;
|
||||
}
|
||||
|
||||
public function GetHeader()
|
||||
{
|
||||
// Check permissions
|
||||
foreach($this->oSearch->GetSelectedClasses() as $sAlias => $sClass)
|
||||
{
|
||||
if (UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_READ) != UR_ALLOWED_YES)
|
||||
{
|
||||
throw new Exception("You do not have enough permissions to bulk read data of class '$sClass' (alias: $sAlias)");
|
||||
}
|
||||
}
|
||||
|
||||
$oSet = new DBObjectSet($this->oSearch);
|
||||
$this->aStatusInfo['position'] = 0;
|
||||
$this->aStatusInfo['total'] = $oSet->Count();
|
||||
$sData = "<"."?xml version=\"1.0\" encoding=\"UTF-8\"?".">\n<Set>\n";
|
||||
return $sData;
|
||||
}
|
||||
|
||||
public function GetNextChunk(&$aStatus)
|
||||
{
|
||||
$sRetCode = 'run';
|
||||
$iPercentage = 0;
|
||||
|
||||
$iCount = 0;
|
||||
$sData = '';
|
||||
|
||||
$oSet = new DBObjectSet($this->oSearch);
|
||||
$oSet->SetLimit($this->iChunkSize, $this->aStatusInfo['position']);
|
||||
|
||||
$aClasses = $this->oSearch->GetSelectedClasses();
|
||||
$aAuthorizedClasses = array();
|
||||
$aClass2Attributes = array();
|
||||
foreach($aClasses as $sAlias => $sClassName)
|
||||
{
|
||||
if (UserRights::IsActionAllowed($sClassName, UR_ACTION_BULK_READ, $oSet) != UR_ALLOWED_NO)
|
||||
{
|
||||
$aAuthorizedClasses[$sAlias] = $sClassName;
|
||||
$aAttributes = array();
|
||||
foreach(MetaModel::ListAttributeDefs($sClassName) as $sAttCode=>$oAttDef)
|
||||
{
|
||||
if ($oAttDef->IsLinkSet() && !$this->aStatusInfo['linksets'])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if ($oAttDef->IsExternalField())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$aAttributes[$sAttCode] = $oAttDef;
|
||||
if ($oAttDef->IsExternalKey())
|
||||
{
|
||||
foreach(MetaModel::ListAttributeDefs($sClassName) as $sSubAttCode=>$oSubAttDef)
|
||||
{
|
||||
if ($oSubAttDef->IsExternalField() && ($oSubAttDef->GetKeyAttCode() == $sAttCode))
|
||||
{
|
||||
$aAttributes[$sAttCode.'_friendlyname'] = MetaModel::GetAttributeDef($sClassName, $sAttCode.'_friendlyname');
|
||||
$aAttributes[$sSubAttCode] = $oSubAttDef;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$aClass2Attributes[$sAlias] = $aAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
$iPreviousTimeLimit = ini_get('max_execution_time');
|
||||
$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
|
||||
|
||||
while ($aObjects = $oSet->FetchAssoc())
|
||||
{
|
||||
set_time_limit(intval($iLoopTimeLimit));
|
||||
if (count($aAuthorizedClasses) > 1)
|
||||
{
|
||||
$sData .= "<Row>\n";
|
||||
}
|
||||
foreach($aAuthorizedClasses as $sAlias => $sClassName)
|
||||
{
|
||||
$oObj = $aObjects[$sAlias];
|
||||
if (is_null($oObj))
|
||||
{
|
||||
$sData .= "<$sClassName alias=\"$sAlias\" id=\"null\">\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
$sClassName = get_class($oObj);
|
||||
$sData .= "<$sClassName alias=\"$sAlias\" id=\"".$oObj->GetKey()."\">\n";
|
||||
}
|
||||
foreach($aClass2Attributes[$sAlias] as $sAttCode=>$oAttDef)
|
||||
{
|
||||
if (is_null($oObj))
|
||||
{
|
||||
$sData .= "<$sAttCode>null</$sAttCode>\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
$sValue = $oObj->GetAsXML($sAttCode, $this->bLocalizeOutput);
|
||||
$sData .= "<$sAttCode>$sValue</$sAttCode>\n";
|
||||
}
|
||||
}
|
||||
$sData .= "</$sClassName>\n";
|
||||
}
|
||||
if (count($aAuthorizedClasses) > 1)
|
||||
{
|
||||
$sData .= "</Row>\n";
|
||||
}
|
||||
$iCount++;
|
||||
}
|
||||
|
||||
set_time_limit(intval($iPreviousTimeLimit));
|
||||
$this->aStatusInfo['position'] += $this->iChunkSize;
|
||||
if ($this->aStatusInfo['total'] == 0)
|
||||
{
|
||||
$iPercentage = 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
$iPercentage = floor(min(100.0, 100.0*$this->aStatusInfo['position']/$this->aStatusInfo['total']));
|
||||
}
|
||||
|
||||
if ($iCount < $this->iChunkSize)
|
||||
{
|
||||
$sRetCode = 'done';
|
||||
}
|
||||
|
||||
$aStatus = array('code' => $sRetCode, 'message' => Dict::S('Core:BulkExport:RetrievingData'), 'percentage' => $iPercentage);
|
||||
return $sData;
|
||||
}
|
||||
|
||||
public function GetFooter()
|
||||
{
|
||||
$sData = "</Set>\n";
|
||||
|
||||
return $sData;
|
||||
}
|
||||
|
||||
public function GetSupportedFormats()
|
||||
{
|
||||
return array('xml' => Dict::S('Core:BulkExport:XMLFormat'));
|
||||
}
|
||||
|
||||
public function GetMimeType()
|
||||
{
|
||||
return 'text/xml';
|
||||
}
|
||||
|
||||
public function GetFileExtension()
|
||||
{
|
||||
return 'xml';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user