Integration of the Excel (XLSX) export feature. (Limitation: export.php takes into account neither the "fields" parameter nor the list of fields defined in the QueryPhrasebook when exporting in XLSX format)

SVN:trunk[3398]
This commit is contained in:
Denis Flaven
2014-10-31 17:59:14 +00:00
parent 77cf399c72
commit 2c59fb894f
13 changed files with 1678 additions and 178 deletions

View File

@@ -0,0 +1,536 @@
<?php
require_once('xlsxwriter.class.php');
class ExcelExporter
{
protected $sToken;
protected $aStatistics;
protected $sState;
protected $fStartTime;
protected $oSearch;
protected $aObjectsIDs;
protected $aTableHeaders;
protected $aAuthorizedClasses;
protected $iChunkSize = 1000;
protected $iPosition;
protected $sOutputFilePath;
protected $bAdvancedMode;
public function __construct($sToken = null)
{
$this->aStatistics = array(
'objects_count' => 0,
'total_duration' => 0,
'data_retrieval_duration' => 0,
'excel_build_duration' => 0,
'excel_write_duration' => 0,
'peak_memory_usage' => 0,
);
$this->fStartTime = microtime(true);
$this->oSearch = null;
$this->sState = 'new';
$this->aObjectsIDs = array();
$this->iPosition = 0;
$this->aAuthorizedClasses = null;
$this->aTableHeaders = null;
$this->sOutputFilePath = null;
$this->bAdvancedMode = false;
$this->CheckDataDir();
if ($sToken == null)
{
$this->sToken = $this->GetNewToken();
}
else
{
$this->sToken = $sToken;
$this->ReloadState();
}
}
public function __destruct()
{
if (($this->sState != 'done') && ($this->sState != 'error') && ($this->sToken != null))
{
// Operation in progress, save the state
$this->SaveState();
}
else
{
// Operation completed, cleanup the temp files
@unlink($this->GetStateFile());
@unlink($this->GetDataFile());
}
self::CleanupOldFiles();
}
public function SetChunkSize($iChunkSize)
{
$this->iChunkSize = $iChunkSize;
}
public function SetOutputFilePath($sDestFilePath)
{
$this->sOutputFilePath = $sDestFilePath;
}
public function SetAdvancedMode($bAdvanced)
{
$this->bAdvancedMode = $bAdvanced;
}
public function SaveState()
{
$aState = array(
'state' => $this->sState,
'statistics' => $this->aStatistics,
'filter' => $this->oSearch->serialize(),
'position' => $this->iPosition,
'chunk_size' => $this->iChunkSize,
'object_ids' => $this->aObjectsIDs,
'output_file_path' => $this->sOutputFilePath,
'advanced_mode' => $this->bAdvancedMode,
);
file_put_contents($this->GetStateFile(), json_encode($aState));
return $this->sToken;
}
public function ReloadState()
{
if ($this->sToken == null)
{
throw new Exception('ExcelExporter not initialized with a token, cannot reload state');
}
if (!file_exists($this->GetStateFile()))
{
throw new Exception("ExcelExporter: missing status file '".$this->GetStateFile()."', cannot reload state.");
}
$sJson = file_get_contents($this->GetStateFile());
$aState = json_decode($sJson, true);
if ($aState === null)
{
throw new Exception("ExcelExporter:corrupted status file '".$this->GetStateFile()."', not a JSON, cannot reload state.");
}
$this->sState = $aState['state'];
$this->aStatistics = $aState['statistics'];
$this->oSearch = DBObjectSearch::unserialize($aState['filter']);
$this->iPosition = $aState['position'];
$this->iChunkSize = $aState['chunk_size'];
$this->aObjectsIDs = $aState['object_ids'];
$this->sOutputFilePath = $aState['output_file_path'];
$this->bAdvancedMode = $aState['advanced_mode'];
}
public function SetObjectList($oSearch)
{
$this->oSearch = $oSearch;
}
public function Run()
{
$sCode = 'error';
$iPercentage = 100;
$sMessage = Dict::Format('ExcelExporter:ErrorUnexpected_State', $this->sState);
$fTime = microtime(true);
try
{
switch($this->sState)
{
case 'new':
$oIDSet = new DBObjectSet($this->oSearch);
$oIDSet->OptimizeColumnLoad(array('id'));
$this->aObjectsIDs = array();
while($oObj = $oIDSet->Fetch())
{
$this->aObjectsIDs[] = $oObj->GetKey();
}
$sCode = 'retrieving-data';
$iPercentage = 5;
$sMessage = Dict::S('ExcelExporter:RetrievingData');
$this->iPosition = 0;
$this->aStatistics['objects_count'] = count($this->aObjectsIDs);
$this->aStatistics['data_retrieval_duration'] += microtime(true) - $fTime;
// The first line of the file is the "headers" specifying the label and the type of each column
$this->GetFieldsList($oIDSet, $this->bAdvancedMode);
$sRow = json_encode($this->aTableHeaders);
$hFile = @fopen($this->GetDataFile(), 'ab');
if ($hFile === false)
{
throw new Exception('ExcelExporter: Failed to open temporary data file: "'.$this->GetDataFile().'" for writing.');
}
fwrite($hFile, $sRow."\n");
fclose($hFile);
// Next state
$this->sState = 'retrieving-data';
break;
case 'retrieving-data':
$oCurrentSearch = clone $this->oSearch;
$aIDs = array_slice($this->aObjectsIDs, $this->iPosition, $this->iChunkSize);
$oCurrentSearch->AddCondition('id', $aIDs, 'IN');
$hFile = @fopen($this->GetDataFile(), 'ab');
if ($hFile === false)
{
throw new Exception('ExcelExporter: Failed to open temporary data file: "'.$this->GetDataFile().'" for writing.');
}
$oSet = new DBObjectSet($oCurrentSearch);
$this->GetFieldsList($oSet, $this->bAdvancedMode);
while($aObjects = $oSet->FetchAssoc())
{
$aRow = array();
foreach($this->aAuthorizedClasses as $sAlias => $sClassName)
{
$oObj = $aObjects[$sAlias];
if ($this->bAdvancedMode)
{
$aRow[] = $oObj->GetKey();
}
foreach($this->aFieldsList[$sAlias] as $sAttCodeEx => $oAttDef)
{
$value = $oObj->Get($sAttCodeEx);
if ($value instanceOf ormCaseLog)
{
// 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!
$sExcelVal = trim(preg_replace('/========== ([^=]+) ============/', '********** $1 ************', $value->GetText()));
}
else
{
$sExcelVal = $oAttDef->GetEditValue($value, $oObj);
}
$aRow[] = $sExcelVal;
}
}
$sRow = json_encode($aRow);
fwrite($hFile, $sRow."\n");
}
fclose($hFile);
if (($this->iPosition + $this->iChunkSize) > count($this->aObjectsIDs))
{
// Next state
$this->sState = 'building-excel';
$sCode = 'building-excel';
$iPercentage = 80;
$sMessage = Dict::S('ExcelExporter:BuildingExcelFile');
}
else
{
$sCode = 'retrieving-data';
$this->iPosition += $this->iChunkSize;
$iPercentage = 5 + round(75 * ($this->iPosition / count($this->aObjectsIDs)));
$sMessage = Dict::S('ExcelExporter:RetrievingData');
}
break;
case 'building-excel':
$hFile = @fopen($this->GetDataFile(), 'rb');
if ($hFile === false)
{
throw new Exception('ExcelExporter: Failed to open temporary data file: "'.$this->GetDataFile().'" for reading.');
}
$sHeaders = fgets($hFile);
$aHeaders = json_decode($sHeaders, true);
$aData = array();
while($sLine = fgets($hFile))
{
$aRow = json_decode($sLine);
$aData[] = $aRow;
}
fclose($hFile);
@unlink($this->GetDataFile());
$fStartExcel = microtime(true);
$writer = new XLSXWriter();
$writer->setAuthor(UserRights::GetUserFriendlyName());
$writer->writeSheet($aData,'Sheet1', $aHeaders);
$fExcelTime = microtime(true) - $fStartExcel;
$this->aStatistics['excel_build_duration'] = $fExcelTime;
$fTime = microtime(true);
$writer->writeToFile($this->GetExcelFilePath());
$fExcelSaveTime = microtime(true) - $fTime;
$this->aStatistics['excel_write_duration'] = $fExcelSaveTime;
// Next state
$this->sState = 'done';
$sCode = 'done';
$iPercentage = 100;
$sMessage = Dict::S('ExcelExporter:Done');
break;
case 'done':
$this->sState = 'done';
$sCode = 'done';
$iPercentage = 100;
$sMessage = Dict::S('ExcelExporter:Done');
break;
}
}
catch(Exception $e)
{
$sCode = 'error';
$sMessage = $e->getMessage();
}
$this->aStatistics['total_duration'] += microtime(true) - $fTime;
$peak_memory = memory_get_peak_usage(true);
if ($peak_memory > $this->aStatistics['peak_memory_usage'])
{
$this->aStatistics['peak_memory_usage'] = $peak_memory;
}
return array(
'code' => $sCode,
'message' => $sMessage,
'percentage' => $iPercentage,
);
}
public function GetExcelFilePath()
{
if ($this->sOutputFilePath == null)
{
return APPROOT.'data/bulk_export/'.$this->sToken.'.xlsx';
}
else
{
return $this->sOutputFilePath;
}
}
public static function GetExcelFileFromToken($sToken)
{
return @file_get_contents(APPROOT.'data/bulk_export/'.$sToken.'.xlsx');
}
public static function CleanupFromToken($sToken)
{
@unlink(APPROOT.'data/bulk_export/'.$sToken.'.status');
@unlink(APPROOT.'data/bulk_export/'.$sToken.'.data');
@unlink(APPROOT.'data/bulk_export/'.$sToken.'.xlsx');
}
public function Cleanup()
{
self::CleanupFromToken($this->sToken);
}
/**
* Delete all files in the data/bulk_export directory which are older than 1 day
* unless a different delay is configured.
*/
public static function CleanupOldFiles()
{
$aFiles = glob(APPROOT.'data/bulk_export/*.*');
$iDelay = MetaModel::GetConfig()->Get('xlsx_exporter_cleanup_old_files_delay');
if($iDelay > 0)
{
foreach($aFiles as $sFile)
{
$iModificationTime = filemtime($sFile);
if($iModificationTime < (time() - $iDelay))
{
// Temporary files older than one day are deleted
//echo "Supposed to delete: '".$sFile." (Unix Modification Time: $iModificationTime)'\n";
@unlink($sFile);
}
}
}
}
public function DisplayStatistics(Page $oPage)
{
$aStats = array(
'Number of objects exported' => $this->aStatistics['objects_count'],
'Total export duration' => sprintf('%.3f s', $this->aStatistics['total_duration']),
'Data retrieval duration' => sprintf('%.3f s', $this->aStatistics['data_retrieval_duration']),
'Excel build duration' => sprintf('%.3f s', $this->aStatistics['excel_build_duration']),
'Excel write duration' => sprintf('%.3f s', $this->aStatistics['excel_write_duration']),
'Peak memory usage' => self::HumanDisplay($this->aStatistics['peak_memory_usage']),
);
if ($oPage instanceof CLIPage)
{
$oPage->add($this->GetStatistics('text'));
}
else
{
$oPage->add($this->GetStatistics('html'));
}
}
public function GetStatistics($sFormat = 'html')
{
$sStats = '';
$aStats = array(
'Number of objects exported' => $this->aStatistics['objects_count'],
'Total export duration' => sprintf('%.3f s', $this->aStatistics['total_duration']),
'Data retrieval duration' => sprintf('%.3f s', $this->aStatistics['data_retrieval_duration']),
'Excel build duration' => sprintf('%.3f s', $this->aStatistics['excel_build_duration']),
'Excel write duration' => sprintf('%.3f s', $this->aStatistics['excel_write_duration']),
'Peak memory usage' => self::HumanDisplay($this->aStatistics['peak_memory_usage']),
);
if ($sFormat == 'text')
{
foreach($aStats as $sLabel => $sValue)
{
$sStats .= "+------------------------------+----------+\n";
$sStats .= sprintf("|%-30s|%10s|\n", $sLabel, $sValue);
}
$sStats .= "+------------------------------+----------+";
}
else
{
$sStats .= '<table><tbody>';
foreach($aStats as $sLabel => $sValue)
{
$sStats .= "<tr><td>$sLabel</td><td>$sValue</td></tr>";
}
$sStats .= '</tbody></table>';
}
return $sStats;
}
public static function HumanDisplay($iSize)
{
$aUnits = array('B','KB','MB','GB','TB','PB');
return @round($iSize/pow(1024,($i=floor(log($iSize,1024)))),2).' '.$aUnits[$i];
}
protected function CheckDataDir()
{
if(!is_dir(APPROOT."data/bulk_export"))
{
@mkdir(APPROOT."data/bulk_export", 0777, true /* recursive */);
clearstatcache();
}
if (!is_writable(APPROOT."data/bulk_export"))
{
throw new Exception('Data directory "'.APPROOT.'data/bulk_export" could not be written.');
}
}
protected function GetStateFile($sToken = null)
{
if ($sToken == null)
{
$sToken = $this->sToken;
}
return APPROOT."data/bulk_export/$sToken.status";
}
protected function GetDataFile()
{
return APPROOT.'data/bulk_export/'.$this->sToken.'.data';
}
protected function GetNewToken()
{
$iNum = rand();
do
{
$iNum++;
$sToken = sprintf("%08x", $iNum);
$sFileName = $this->GetStateFile($sToken);
$hFile = @fopen($sFileName, 'x');
}
while($hFile === false);
fclose($hFile);
return $sToken;
}
protected function GetFieldsList($oSet, $bFieldsAdvanced = false, $bLocalize = true, $aFields = null)
{
$this->aFieldsList = array();
$oAppContext = new ApplicationContext();
$aClasses = $oSet->GetFilter()->GetSelectedClasses();
$this->aAuthorizedClasses = array();
foreach($aClasses as $sAlias => $sClassName)
{
if (UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $oSet) && (UR_ALLOWED_YES || UR_ALLOWED_DEPENDS))
{
$this->aAuthorizedClasses[$sAlias] = $sClassName;
}
}
$aAttribs = array();
$this->aTableHeaders = array();
foreach($this->aAuthorizedClasses as $sAlias => $sClassName)
{
$aList[$sAlias] = array();
foreach(MetaModel::ListAttributeDefs($sClassName) as $sAttCode => $oAttDef)
{
if (is_null($aFields) || (count($aFields) == 0))
{
// Standard list of attributes (no link sets)
if ($oAttDef->IsScalar() && ($oAttDef->IsWritable() || $oAttDef->IsExternalField()))
{
$sAttCodeEx = $oAttDef->IsExternalField() ? $oAttDef->GetKeyAttCode().'->'.$oAttDef->GetExtAttCode() : $sAttCode;
if ($oAttDef->IsExternalKey(EXTKEY_ABSOLUTE))
{
if ($bFieldsAdvanced)
{
$aList[$sAlias][$sAttCodeEx] = $oAttDef;
if ($oAttDef->IsExternalKey(EXTKEY_RELATIVE))
{
$sRemoteClass = $oAttDef->GetTargetClass();
foreach(MetaModel::GetReconcKeys($sRemoteClass) as $sRemoteAttCode)
{
$this->aFieldsList[$sAlias][$sAttCode.'->'.$sRemoteAttCode] = MetaModel::GetAttributeDef($sRemoteClass, $sRemoteAttCode);
}
}
}
}
else
{
// Any other attribute
$this->aFieldsList[$sAlias][$sAttCodeEx] = $oAttDef;
}
}
}
else
{
// User defined list of attributes
if (in_array($sAttCode, $aFields) || in_array($sAlias.'.'.$sAttCode, $aFields))
{
$this->aFieldsList[$sAlias][$sAttCode] = $oAttDef;
}
}
}
if ($bFieldsAdvanced)
{
$this->aTableHeaders['id'] = '0';
}
foreach($this->aFieldsList[$sAlias] as $sAttCodeEx => $oAttDef)
{
$sLabel = $bLocalize ? MetaModel::GetLabel($sClassName, $sAttCodeEx, isset($aParams['showMandatoryFields'])) : $sAttCodeEx;
if($oAttDef instanceof AttributeDateTime)
{
$this->aTableHeaders[$sLabel] = 'datetime';
}
else
{
$this->aTableHeaders[$sLabel] = 'string';
}
}
}
}
}

View File

@@ -783,11 +783,16 @@ class utils
$sOQL = addslashes($param->GetFilter()->ToOQL(true));
$sFilter = urlencode($param->GetFilter()->serialize());
$sUrl = utils::GetAbsoluteUrlAppRoot()."pages/$sUIPage?operation=search&filter=".$sFilter."&{$sContext}";
$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/xlsx-export.js');
$sXlsxFilter = $param->GetFilter()->serialize();
$sXlsxJSFilter = addslashes($sXlsxFilter);
$aResult = array(
new SeparatorPopupMenuItem(),
// Static menus: Email this page, CSV Export & Add to Dashboard
new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), "mailto:?body=".urlencode($sUrl).' '), // Add an extra space to make it work in Outlook
new URLPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), $sUrl."&format=csv"),
new JSPopupMenuItem('xlsx-export', Dict::S('ExcelExporter:ExportMenu'), "XlsxExportDialog('$sXlsxJSFilter');", array()),
new JSPopupMenuItem('UI:Menu:AddToDashboard', Dict::S('UI:Menu:AddToDashboard'), "DashletCreationDlg('$sOQL')"),
new JSPopupMenuItem('UI:Menu:ShortcutList', Dict::S('UI:Menu:ShortcutList'), "ShortcutListDlg('$sOQL', '$sDataTableId', '$sContext')"),
);
@@ -802,11 +807,15 @@ class utils
$sUIPage = cmdbAbstractObject::ComputeStandardUIPage(get_class($oObj));
$oAppContext = new ApplicationContext();
$sContext = $oAppContext->GetForLink();
$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/xlsx-export.js');
$sXlsxFilter = $param->GetFilter()->serialize();
$sXlsxJSFilter = addslashes($sXlsxFilter);
$aResult = array(
new SeparatorPopupMenuItem(),
// Static menus: Email this page & CSV Export
new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), "mailto:?subject=".urlencode($oObj->GetRawName())."&body=".urlencode($sUrl).' '), // Add an extra space to make it work in Outlook
new URLPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), utils::GetAbsoluteUrlAppRoot()."pages/$sUIPage?operation=search&filter=".urlencode($sFilter)."&format=csv&{$sContext}"),
new JSPopupMenuItem('xlsx-export', Dict::S('ExcelExporter:ExportMenu'), "XlsxExportDialog('$sXlsxJSFilter');", array()),
);
break;

View File

@@ -0,0 +1,456 @@
<?php
/* @author Mark Jones
* @license MIT License
* */
if (!class_exists('ZipArchive')) { throw new Exception('ZipArchive not found'); }
Class XLSXWriter
{
//------------------------------------------------------------------
protected $author ='Doc Author';
protected $sheets_meta = array();
protected $shared_strings = array();//unique set
protected $shared_string_count = 0;//count of non-unique references to the unique set
protected $temp_files = array();
public function __construct(){}
public function setAuthor($author='') { $this->author=$author; }
public function __destruct()
{
if (!empty($this->temp_files)) {
foreach($this->temp_files as $temp_file) {
@unlink($temp_file);
}
}
}
protected function tempFilename()
{
$filename = tempnam("/tmp", "xlsx_writer_");
$this->temp_files[] = $filename;
return $filename;
}
public function writeToStdOut()
{
$temp_file = $this->tempFilename();
self::writeToFile($temp_file);
readfile($temp_file);
}
public function writeToString()
{
$temp_file = $this->tempFilename();
self::writeToFile($temp_file);
$string = file_get_contents($temp_file);
return $string;
}
public function writeToFile($filename)
{
@unlink($filename);//if the zip already exists, overwrite it
$zip = new ZipArchive();
if (empty($this->sheets_meta)) { self::log("Error in ".__CLASS__."::".__FUNCTION__.", no worksheets defined."); return; }
if (!$zip->open($filename, ZipArchive::CREATE)) { self::log("Error in ".__CLASS__."::".__FUNCTION__.", unable to create zip."); return; }
$zip->addEmptyDir("docProps/");
$zip->addFromString("docProps/app.xml" , self::buildAppXML() );
$zip->addFromString("docProps/core.xml", self::buildCoreXML());
$zip->addEmptyDir("_rels/");
$zip->addFromString("_rels/.rels", self::buildRelationshipsXML());
$zip->addEmptyDir("xl/worksheets/");
foreach($this->sheets_meta as $sheet_meta) {
$zip->addFile($sheet_meta['filename'], "xl/worksheets/".$sheet_meta['xmlname'] );
}
if (!empty($this->shared_strings)) {
$zip->addFile($this->writeSharedStringsXML(), "xl/sharedStrings.xml" ); //$zip->addFromString("xl/sharedStrings.xml", self::buildSharedStringsXML() );
}
$zip->addFromString("xl/workbook.xml" , self::buildWorkbookXML() );
$zip->addFile($this->writeStylesXML(), "xl/styles.xml" ); //$zip->addFromString("xl/styles.xml" , self::buildStylesXML() );
$zip->addFromString("[Content_Types].xml" , self::buildContentTypesXML() );
$zip->addEmptyDir("xl/_rels/");
$zip->addFromString("xl/_rels/workbook.xml.rels", self::buildWorkbookRelsXML() );
$zip->close();
}
public function writeSheet(array $data, $sheet_name='', array $header_types=array() )
{
$data = empty($data) ? array( array('') ) : $data;
$sheet_filename = $this->tempFilename();
$sheet_default = 'Sheet'.(count($this->sheets_meta)+1);
$sheet_name = !empty($sheet_name) ? $sheet_name : $sheet_default;
$this->sheets_meta[] = array('filename'=>$sheet_filename, 'sheetname'=>$sheet_name ,'xmlname'=>strtolower($sheet_default).".xml" );
$header_offset = empty($header_types) ? 0 : 1;
$row_count = count($data) + $header_offset;
$column_count = count($data[self::array_first_key($data)]);
$max_cell = self::xlsCell( $row_count-1, $column_count-1 );
$tabselected = count($this->sheets_meta)==1 ? 'true' : 'false';//only first sheet is selected
$cell_formats_arr = empty($header_types) ? array_fill(0, $column_count, 'string') : array_values($header_types);
$header_row = empty($header_types) ? array() : array_keys($header_types);
$fd = fopen($sheet_filename, "w+");
if ($fd===false) { self::log("write failed in ".__CLASS__."::".__FUNCTION__."."); return; }
fwrite($fd,'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n");
fwrite($fd,'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">');
fwrite($fd, '<sheetPr filterMode="false">');
fwrite($fd, '<pageSetUpPr fitToPage="false"/>');
fwrite($fd, '</sheetPr>');
fwrite($fd, '<dimension ref="A1:'.$max_cell.'"/>');
fwrite($fd, '<sheetViews>');
fwrite($fd, '<sheetView colorId="64" defaultGridColor="true" rightToLeft="false" showFormulas="false" showGridLines="true" showOutlineSymbols="true" showRowColHeaders="true" showZeros="true" tabSelected="'.$tabselected.'" topLeftCell="A1" view="normal" windowProtection="false" workbookViewId="0" zoomScale="100" zoomScaleNormal="100" zoomScalePageLayoutView="100">');
fwrite($fd, '<selection activeCell="A1" activeCellId="0" pane="topLeft" sqref="A1"/>');
fwrite($fd, '</sheetView>');
fwrite($fd, '</sheetViews>');
fwrite($fd, '<cols>');
fwrite($fd, '<col collapsed="false" hidden="false" max="1025" min="1" style="0" width="19"/>');
fwrite($fd, '</cols>');
fwrite($fd, '<sheetData>');
if (!empty($header_row))
{
fwrite($fd, '<row collapsed="false" customFormat="false" customHeight="false" hidden="false" ht="12.1" outlineLevel="0" r="'.(1).'">');
foreach($header_row as $k=>$v)
{
$this->writeCell($fd, 0, $k, $v, $cell_format='string');
}
fwrite($fd, '</row>');
}
foreach($data as $i=>$row)
{
fwrite($fd, '<row collapsed="false" customFormat="false" customHeight="false" hidden="false" ht="12.1" outlineLevel="0" r="'.($i+$header_offset+1).'">');
foreach($row as $k=>$v)
{
$this->writeCell($fd, $i+$header_offset, $k, $v, $cell_formats_arr[$k]);
}
fwrite($fd, '</row>');
}
fwrite($fd, '</sheetData>');
fwrite($fd, '<printOptions headings="false" gridLines="false" gridLinesSet="true" horizontalCentered="false" verticalCentered="false"/>');
fwrite($fd, '<pageMargins left="0.5" right="0.5" top="1.0" bottom="1.0" header="0.5" footer="0.5"/>');
fwrite($fd, '<pageSetup blackAndWhite="false" cellComments="none" copies="1" draft="false" firstPageNumber="1" fitToHeight="1" fitToWidth="1" horizontalDpi="300" orientation="portrait" pageOrder="downThenOver" paperSize="1" scale="100" useFirstPageNumber="true" usePrinterDefaults="false" verticalDpi="300"/>');
fwrite($fd, '<headerFooter differentFirst="false" differentOddEven="false">');
fwrite($fd, '<oddHeader>&amp;C&amp;&quot;Times New Roman,Regular&quot;&amp;12&amp;A</oddHeader>');
fwrite($fd, '<oddFooter>&amp;C&amp;&quot;Times New Roman,Regular&quot;&amp;12Page &amp;P</oddFooter>');
fwrite($fd, '</headerFooter>');
fwrite($fd,'</worksheet>');
fclose($fd);
}
protected function writeCell($fd, $row_number, $column_number, $value, $cell_format)
{
static $styles = array('money'=>1,'dollar'=>1,'datetime'=>2,'date'=>3,'string'=>0);
$cell = self::xlsCell($row_number, $column_number);
$s = isset($styles[$cell_format]) ? $styles[$cell_format] : '0';
if (is_numeric($value)) {
fwrite($fd,'<c r="'.$cell.'" s="'.$s.'" t="n"><v>'.($value*1).'</v></c>');//int,float, etc
} else if ($cell_format=='date') {
fwrite($fd,'<c r="'.$cell.'" s="'.$s.'" t="n"><v>'.intval(self::convert_date_time($value)).'</v></c>');
} else if ($cell_format=='datetime') {
if ($value === '') {
fwrite($fd,'<c r="'.$cell.'" s="0"/>');
} else {
fwrite($fd,'<c r="'.$cell.'" s="'.$s.'" t="n"><v>'.self::convert_date_time($value).'</v></c>');
}
} else if ($value==''){
fwrite($fd,'<c r="'.$cell.'" s="'.$s.'"/>');
} else if ($value{0}=='='){
fwrite($fd,'<c r="'.$cell.'" s="'.$s.'" t="s"><f>'.self::xmlspecialchars($value).'</f></c>');
} else if ($value!==''){
fwrite($fd,'<c r="'.$cell.'" s="'.$s.'" t="s"><v>'.self::xmlspecialchars($this->setSharedString($value)).'</v></c>');
}
}
protected function writeStylesXML()
{
$tempfile = $this->tempFilename();
$fd = fopen($tempfile, "w+");
if ($fd===false) { self::log("write failed in ".__CLASS__."::".__FUNCTION__."."); return; }
fwrite($fd, '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n");
fwrite($fd, '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">');
fwrite($fd, '<numFmts count="4">');
fwrite($fd, '<numFmt formatCode="GENERAL" numFmtId="164"/>');
fwrite($fd, '<numFmt formatCode="[$$-1009]#,##0.00;[RED]\-[$$-1009]#,##0.00" numFmtId="165"/>');
fwrite($fd, '<numFmt formatCode="YYYY-MM-DD\ HH:MM:SS" numFmtId="166"/>');
fwrite($fd, '<numFmt formatCode="YYYY-MM-DD" numFmtId="167"/>');
fwrite($fd, '</numFmts>');
fwrite($fd, '<fonts count="4">');
fwrite($fd, '<font><name val="Arial"/><charset val="1"/><family val="2"/><sz val="10"/></font>');
fwrite($fd, '<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
fwrite($fd, '<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
fwrite($fd, '<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
fwrite($fd, '</fonts>');
fwrite($fd, '<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>');
fwrite($fd, '<borders count="1"><border diagonalDown="false" diagonalUp="false"><left/><right/><top/><bottom/><diagonal/></border></borders>');
fwrite($fd, '<cellStyleXfs count="15">');
fwrite($fd, '<xf applyAlignment="true" applyBorder="true" applyFont="true" applyProtection="true" borderId="0" fillId="0" fontId="0" numFmtId="164">');
fwrite($fd, '<alignment horizontal="general" indent="0" shrinkToFit="false" textRotation="0" vertical="bottom" wrapText="false"/>');
fwrite($fd, '<protection hidden="false" locked="true"/>');
fwrite($fd, '</xf>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="2" numFmtId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="2" numFmtId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
//fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="43"/>');
//fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="41"/>');
//fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="44"/>');
//fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="42"/>');
//fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="9"/>');
fwrite($fd, '</cellStyleXfs>');
fwrite($fd, '<cellXfs count="4">');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="164" xfId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="165" xfId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="166" xfId="0"/>');
fwrite($fd, '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="167" xfId="0"/>');
fwrite($fd, '</cellXfs>');
fwrite($fd, '<cellStyles count="1">');
fwrite($fd, '<cellStyle builtinId="0" customBuiltin="false" name="Normal" xfId="0"/>');
//fwrite($fd, '<cellStyle builtinId="3" customBuiltin="false" name="Comma" xfId="15"/>');
//fwrite($fd, '<cellStyle builtinId="6" customBuiltin="false" name="Comma [0]" xfId="16"/>');
//fwrite($fd, '<cellStyle builtinId="4" customBuiltin="false" name="Currency" xfId="17"/>');
//fwrite($fd, '<cellStyle builtinId="7" customBuiltin="false" name="Currency [0]" xfId="18"/>');
//fwrite($fd, '<cellStyle builtinId="5" customBuiltin="false" name="Percent" xfId="19"/>');
fwrite($fd, '</cellStyles>');
fwrite($fd, '</styleSheet>');
fclose($fd);
return $tempfile;
}
protected function setSharedString($v)
{
// Strip control characters which Excel does not seem to like...
$v = preg_replace('/[\x00-\x09\x0B\x0C\x0E-\x1F]/u', '', $v);
if (isset($this->shared_strings[$v]))
{
$string_value = $this->shared_strings[$v];
}
else
{
$string_value = count($this->shared_strings);
$this->shared_strings[$v] = $string_value;
}
$this->shared_string_count++;//non-unique count
return $string_value;
}
protected function writeSharedStringsXML()
{
$tempfile = $this->tempFilename();
$fd = fopen($tempfile, "w+");
if ($fd===false) { self::log("write failed in ".__CLASS__."::".__FUNCTION__."."); return; }
fwrite($fd,'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n");
fwrite($fd,'<sst count="'.($this->shared_string_count).'" uniqueCount="'.count($this->shared_strings).'" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">');
foreach($this->shared_strings as $s=>$c)
{
fwrite($fd,'<si><t>'.self::xmlspecialchars($s).'</t></si>');
}
fwrite($fd, '</sst>');
fclose($fd);
return $tempfile;
}
protected function buildAppXML()
{
$app_xml="";
$app_xml.='<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n";
$app_xml.='<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><TotalTime>0</TotalTime></Properties>';
return $app_xml;
}
protected function buildCoreXML()
{
$core_xml="";
$core_xml.='<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n";
$core_xml.='<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">';
$core_xml.='<dcterms:created xsi:type="dcterms:W3CDTF">'.date("Y-m-d\TH:i:s.00\Z").'</dcterms:created>';//$date_time = '2013-07-25T15:54:37.00Z';
$core_xml.='<dc:creator>'.self::xmlspecialchars($this->author).'</dc:creator>';
$core_xml.='<cp:revision>0</cp:revision>';
$core_xml.='</cp:coreProperties>';
return $core_xml;
}
protected function buildRelationshipsXML()
{
$rels_xml="";
$rels_xml.='<?xml version="1.0" encoding="UTF-8"?>'."\n";
$rels_xml.='<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">';
$rels_xml.='<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>';
$rels_xml.='<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>';
$rels_xml.='<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>';
$rels_xml.="\n";
$rels_xml.='</Relationships>';
return $rels_xml;
}
protected function buildWorkbookXML()
{
$workbook_xml="";
$workbook_xml.='<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n";
$workbook_xml.='<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">';
$workbook_xml.='<fileVersion appName="Calc"/><workbookPr backupFile="false" showObjects="all" date1904="false"/><workbookProtection/>';
$workbook_xml.='<bookViews><workbookView activeTab="0" firstSheet="0" showHorizontalScroll="true" showSheetTabs="true" showVerticalScroll="true" tabRatio="212" windowHeight="8192" windowWidth="16384" xWindow="0" yWindow="0"/></bookViews>';
$workbook_xml.='<sheets>';
foreach($this->sheets_meta as $i=>$sheet_meta) {
$workbook_xml.='<sheet name="'.self::xmlspecialchars($sheet_meta['sheetname']).'" sheetId="'.($i+1).'" state="visible" r:id="rId'.($i+2).'"/>';
}
$workbook_xml.='</sheets>';
$workbook_xml.='<calcPr iterateCount="100" refMode="A1" iterate="false" iterateDelta="0.001"/></workbook>';
return $workbook_xml;
}
protected function buildWorkbookRelsXML()
{
$wkbkrels_xml="";
$wkbkrels_xml.='<?xml version="1.0" encoding="UTF-8"?>'."\n";
$wkbkrels_xml.='<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">';
$wkbkrels_xml.='<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>';
foreach($this->sheets_meta as $i=>$sheet_meta) {
$wkbkrels_xml.='<Relationship Id="rId'.($i+2).'" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/'.($sheet_meta['xmlname']).'"/>';
}
if (!empty($this->shared_strings)) {
$wkbkrels_xml.='<Relationship Id="rId'.(count($this->sheets_meta)+2).'" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>';
}
$wkbkrels_xml.="\n";
$wkbkrels_xml.='</Relationships>';
return $wkbkrels_xml;
}
protected function buildContentTypesXML()
{
$content_types_xml="";
$content_types_xml.='<?xml version="1.0" encoding="UTF-8"?>'."\n";
$content_types_xml.='<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">';
$content_types_xml.='<Override PartName="/_rels/.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
$content_types_xml.='<Override PartName="/xl/_rels/workbook.xml.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
foreach($this->sheets_meta as $i=>$sheet_meta) {
$content_types_xml.='<Override PartName="/xl/worksheets/'.($sheet_meta['xmlname']).'" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>';
}
if (!empty($this->shared_strings)) {
$content_types_xml.='<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>';
}
$content_types_xml.='<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>';
$content_types_xml.='<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>';
$content_types_xml.='<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>';
$content_types_xml.='<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>';
$content_types_xml.="\n";
$content_types_xml.='</Types>';
return $content_types_xml;
}
//------------------------------------------------------------------
/*
* @param $row_number int, zero based
* @param $column_number int, zero based
* @return Cell label/coordinates, ex: A1, C3, AA42
* */
public static function xlsCell($row_number, $column_number)
{
$n = $column_number;
for($r = ""; $n >= 0; $n = intval($n / 26) - 1) {
$r = chr($n%26 + 0x41) . $r;
}
return $r . ($row_number+1);
}
//------------------------------------------------------------------
public static function log($string)
{
file_put_contents("php://stderr", date("Y-m-d H:i:s:").rtrim(is_array($string) ? json_encode($string) : $string)."\n");
}
//------------------------------------------------------------------
public static function xmlspecialchars($val)
{
return str_replace("'", "&#39;", htmlspecialchars($val));
}
//------------------------------------------------------------------
public static function array_first_key(array $arr)
{
reset($arr);
$first_key = key($arr);
return $first_key;
}
//------------------------------------------------------------------
public static function convert_date_time($date_input) //thanks to Excel::Writer::XLSX::Worksheet.pm (perl)
{
$days = 0; # Number of days since epoch
$seconds = 0; # Time expressed as fraction of 24h hours in seconds
$year=$month=$day=0;
$hour=$min =$sec=0;
$date_time = $date_input;
if (preg_match("/(\d{4})\-(\d{2})\-(\d{2})/", $date_time, $matches))
{
list($junk,$year,$month,$day) = $matches;
}
if (preg_match("/(\d{2}):(\d{2}):(\d{2})/", $date_time, $matches))
{
list($junk,$hour,$min,$sec) = $matches;
$seconds = ( $hour * 60 * 60 + $min * 60 + $sec ) / ( 24 * 60 * 60 );
}
//using 1900 as epoch, not 1904, ignoring 1904 special case
# Special cases for Excel.
if ("$year-$month-$day"=='1899-12-31') return $seconds ; # Excel 1900 epoch
if ("$year-$month-$day"=='1900-01-00') return $seconds ; # Excel 1900 epoch
if ("$year-$month-$day"=='1900-02-29') return 60 + $seconds ; # Excel false leapday
# We calculate the date by calculating the number of days since the epoch
# and adjust for the number of leap days. We calculate the number of leap
# days by normalising the year in relation to the epoch. Thus the year 2000
# becomes 100 for 4 and 100 year leapdays and 400 for 400 year leapdays.
$epoch = 1900;
$offset = 0;
$norm = 300;
$range = $year - $epoch;
# Set month days and check for leap year.
$leap = (($year % 400 == 0) || (($year % 4 == 0) && ($year % 100)) ) ? 1 : 0;
$mdays = array( 31, ($leap ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
# Some boundary checks
if($year < $epoch || $year > 9999) return 0;
if($month < 1 || $month > 12) return 0;
if($day < 1 || $day > $mdays[ $month - 1 ]) return 0;
# Accumulate the number of days since the epoch.
$days = $day; # Add days for current month
$days += array_sum( array_slice($mdays, 0, $month-1 ) ); # Add days for past months
$days += $range * 365; # Add days for past years
$days += intval( ( $range ) / 4 ); # Add leapdays
$days -= intval( ( $range + $offset ) / 100 ); # Subtract 100 year leapdays
$days += intval( ( $range + $offset + $norm ) / 400 ); # Add 400 year leapdays
$days -= $leap; # Already counted above
# Adjust for Excel erroneously treating 1900 as a leap year.
if ($days > 59) { $days++;}
return $days + $seconds;
}
//------------------------------------------------------------------
}

View File

@@ -777,6 +777,22 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'xlsx_exporter_cleanup_old_files_delay' => array(
'type' => 'int',
'description' => 'Delay (in seconds) for which to let the exported XLSX files on the server so that the user who initiated the export can download the result',
'default' => 86400,
'value' => '',
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'xlsx_exporter_memory_limit' => array(
'type' => 'string',
'description' => 'Memory limit to use when (interactively) exporting data to Excel',
'default' => '2048M', // Huuuuuuge 2GB!
'value' => '',
'source_of_value' => '',
'show_in_conf_sample' => false,
),
);
public function IsProperty($sPropCode)

View File

@@ -1217,5 +1217,16 @@ When associated with a trigger, each action is given an "order" number, specifyi
'UI:About:Support' => 'Support information',
'UI:About:Licenses' => 'Licenses',
'UI:About:Modules' => 'Installed modules',
'ExcelExporter:ExportMenu' => 'Excel Export...',
'ExcelExporter:ExportDialogTitle' => 'Excel Export',
'ExcelExporter:ExportButton' => 'Export',
'ExcelExporter:DownloadButton' => 'Download %1$s',
'ExcelExporter:RetrievingData' => 'Retrieving data...',
'ExcelExporter:BuildingExcelFile' => 'Building the Excel file...',
'ExcelExporter:Done' => 'Done.',
'ExcelExport:AutoDownload' => 'Start the download automatically when the export is ready',
'ExcelExport:PreparingExport' => 'Preparing the export...',
'ExcelExport:Statistics' => 'Statistics',
));
?>

View File

@@ -1057,5 +1057,16 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
'UI:About:Support' => 'Informations pour le support',
'UI:About:Licenses' => 'Licences',
'UI:About:Modules' => 'Modules installés',
'ExcelExporter:ExportMenu' => 'Exporter pour Excel...',
'ExcelExporter:ExportDialogTitle' => 'Export au format Excel',
'ExcelExporter:ExportButton' => 'Exporter',
'ExcelExporter:DownloadButton' => 'Télécharger %1$s',
'ExcelExporter:RetrievingData' => 'Récupération des données...',
'ExcelExporter:BuildingExcelFile' => 'Construction du fichier Excel...',
'ExcelExporter:Done' => 'Terminé.',
'ExcelExport:AutoDownload' => 'Téléchargement automatique dès que le fichier est prêt',
'ExcelExport:PreparingExport' => 'Préparation de l\'export...',
'ExcelExport:Statistics' => 'Statistiques',
));
?>

BIN
images/xlsx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

185
js/xlsx-export.js Normal file
View File

@@ -0,0 +1,185 @@
// jQuery UI style "widget" for managing the "xlsx-exporter"
$(function()
{
// the widget definition, where "itop" is the namespace,
// "xlsxexporter" the widget name
$.widget( "itop.xlsxexporter",
{
// default options
options:
{
filter: '',
ajax_page_url: '',
labels: {dialog_title: 'Excel Export', export_button: 'Export', cancel_button: 'Cancel', download_button: 'Download', complete: 'Complete', cancelled: 'Cancelled' }
},
// the constructor
_create: function()
{
this.element
.addClass('itop-xlsxexporter');
this.sToken = null;
this.ajaxCall = null;
this.oProgressBar = $('.progress-bar', this.element);
this.oStatusMessage = $('.status-message', this.element);
$('.progress', this.element).hide();
$('.statistics', this.element).hide();
var me = this;
this.element.dialog({
title: this.options.labels.dialog_title,
modal: true,
width: 500,
height: 300,
buttons: [
{ text: this.options.labels.export_button, 'class': 'export-button', click: function() {
me._start();
} },
{ text: this.options.labels.cancel_button, 'class': 'cancel-button', click: function() {
$(this).dialog( "close" );
} },
],
close: function() { me._abort(); $(this).remove(); }
});
},
// events bound via _bind are removed automatically
// revert other modifications here
destroy: function()
{
this.element
.removeClass('itop-xlsxexporter');
},
// _setOptions is called with a hash of all options that are changing
_setOptions: function()
{
this._superApply(arguments);
},
// _setOption is called for each individual option that is changing
_setOption: function( key, value )
{
this._superApply(arguments);
},
_start: function()
{
var me = this;
$('.export-options', this.element).hide();
$('.progress', this.element).show();
var bAdvanced = $('#export-advanced-mode').prop('checked');
this.bAutoDownload = $('#export-auto-download').prop('checked');
$('.export-button', this.element.parent()).button('disable');
this.oProgressBar.progressbar({
value: 0,
change: function() {
var progressLabel = $('.progress-label', me.element);
progressLabel.text( $(this).progressbar( "value" ) + "%" );
},
complete: function() {
var progressLabel = $('.progress-label', me.element);
progressLabel.text( me.options.labels['complete'] );
}
});
//TODO disable the "export" button
this.ajaxCall = $.post(this.options.ajax_page_url, {filter: this.options.filter, operation: 'xlsx_start', advanced: bAdvanced}, function(data) {
this.ajaxCall = null;
if (data && data.status == 'ok')
{
me.sToken = data.token;
me._run();
}
else
{
if (data == null)
{
me.oStatusMessage.html('Unexpected error (operation=xlsx_start).');
me.oProgressBar.progressbar({value: 100});
}
else
{
me.oStatusMessage.html(data.message);
}
}
}, 'json');
},
_abort: function()
{
$('.cancel-button', this.element.parent()).button('disable');
this.oStatusMessage.html(this.options.labels['cancelled']);
this.oProgressBar.progressbar({value: 100});
if (this.sToken != null)
{
// Cancel the operation in progress... or cleanup a completed export
// TODO
if (this.ajaxCall)
{
this.ajaxCall.abort();
this.ajaxClass = null;
}
var me = this;
$.post(this.options.ajax_page_url, {token: this.sToken, operation: 'xlsx_abort'}, function(data) {
me.sToken = null;
});
}
},
_run: function()
{
var me = this;
this.ajaxCall = $.post(this.options.ajax_page_url, {token: this.sToken, operation: 'xlsx_run'}, function(data) {
this.ajaxCall = null;
if (data == null)
{
me.oStatusMessage.html('Unexpected error (operation=xlsx_run).');
me.oProgressBar.progressbar({value: 100});
}
else if (data.status == 'error')
{
me.oStatusMessage.html(data.message);
me.oProgressBar.progressbar({value: 100});
}
else if (data.status == 'done')
{
me.oStatusMessage.html(data.message);
me.oProgressBar.progressbar({value: 100});
$('.stats-data', this.element).html(data.statistics);
me._on_completion();
}
else
{
// continue running the export in the background
me.oStatusMessage.html(data.message);
me.oProgressBar.progressbar({value: data.percentage});
me._run();
}
}, 'json');
},
_on_completion: function()
{
var me = this;
$('.progress', this.element).html('<form class="download-form" method="post" action="'+this.options.ajax_page_url+'"><input type="hidden" name="operation" value="xlsx_download"/><input type="hidden" name="token" value="'+this.sToken+'"/><button type="submit">'+this.options.labels['download_button']+'</button></form>');
$('.download-form button', this.element).button().click(function() { me.sToken = null; window.setTimeout(function() { me.element.dialog('close'); }, 100); return true;});
if (this.bAutoDownload)
{
me.sToken = null;
$('.download-form').submit();
this.element.dialog('close');
}
else
{
$('.statistics', this.element).show();
$('.statistics .stats-toggle', this.element).click(function() { $(this).toggleClass('closed'); });
}
}
});
});
function XlsxExportDialog(sFilter)
{
var sUrl = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php';
$.post(sUrl, {operation: 'xlsx_export_dialog', filter: sFilter}, function(data) {
$('body').append(data);
});
}

View File

@@ -418,6 +418,7 @@ EOF
case 'get_csv_template':
$sClassName = utils::ReadParam('class_name');
$sFormat = utils::ReadParam('format', 'csv');
if (MetaModel::IsValidClass($sClassName))
{
$oSearch = new DBObjectSearch($sClassName);
@@ -429,17 +430,37 @@ EOF
$sDisposition = utils::ReadParam('disposition', 'inline');
if ($sDisposition == 'attachment')
{
switch($sFormat)
{
case 'xlsx':
$oPage = new ajax_page("");
$oPage->SetContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
$oPage->SetContentDisposition('attachment', $sClassDisplayName.'.xlsx');
require_once(APPROOT.'/application/excelexporter.class.inc.php');
$writer = new XLSXWriter();
$writer->setAuthor(UserRights::GetUserFriendlyName());
$aHeaders = array( 0 => explode(',', $sResult)); // comma is the default separator
$writer->writeSheet($aHeaders, $sClassDisplayName, array());
$oPage->add($writer->writeToString());
break;
case 'csv':
default:
$oPage = new CSVPage("");
$oPage->add_header("Content-type: text/csv; charset=utf-8");
$oPage->add_header("Content-disposition: attachment; filename=\"{$sClassDisplayName}.csv\"");
$oPage->no_cache();
$oPage->add($sResult);
}
}
else
{
$oPage = new ajax_page("");
$oPage->no_cache();
$oPage->add('<p style="text-align:center"><a style="text-decoration:none" href="'.utils::GetAbsoluteUrlAppRoot().'pages/ajax.csvimport.php?operation=get_csv_template&disposition=attachment&class_name='.$sClassName.'"><img border="0" src="../images/csv.png"><br/>'.$sClassDisplayName.'.csv</a></p>');
$oPage->add('<p style="text-align:center">');
$oPage->add('<div style="display:inline-block;margin:0.5em;"><a style="text-decoration:none" href="'.utils::GetAbsoluteUrlAppRoot().'pages/ajax.csvimport.php?operation=get_csv_template&disposition=attachment&class_name='.$sClassName.'"><img border="0" src="../images/csv.png"><br/>'.$sClassDisplayName.'.csv</a></div>');
$oPage->add('<div style="display:inline-block;margin:0.5em;"><a style="text-decoration:none" href="'.utils::GetAbsoluteUrlAppRoot().'pages/ajax.csvimport.php?operation=get_csv_template&disposition=attachment&format=xlsx&class_name='.$sClassName.'"><img border="0" src="../images/xlsx.png"><br/>'.$sClassDisplayName.'.xlsx</a></div>');
$oPage->add('</p>');
$oPage->add('<p><textarea rows="5" cols="100">'.$sResult.'</textarea></p>');
}
}

View File

@@ -32,6 +32,7 @@ require_once(APPROOT.'/application/wizardhelper.class.inc.php');
require_once(APPROOT.'/application/ui.linkswidget.class.inc.php');
require_once(APPROOT.'/application/ui.extkeywidget.class.inc.php');
require_once(APPROOT.'/application/datatable.class.inc.php');
require_once(APPROOT.'/application/excelexporter.class.inc.php');
try
{
@@ -1608,6 +1609,119 @@ EOF
);
break;
case 'xlsx_export_dialog':
$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
$oPage->SetContentType('text/html');
$oPage->add(
<<<EOF
<style>
.ui-progressbar {
position: relative;
}
.progress-label {
position: absolute;
left: 50%;
top: 1px;
font-size: 11pt;
}
.download-form button {
display:block;
margin-left: auto;
margin-right: auto;
margin-top: 2em;
}
.ui-progressbar-value {
background: url(../setup/orange-progress.gif);
}
.progress-bar {
height: 20px;
}
.statistics > div {
padding-left: 16px;
cursor: pointer;
font-size: 10pt;
background: url(../images/minus.gif) 0 2px no-repeat;
}
.statistics > div.closed {
padding-left: 16px;
background: url(../images/plus.gif) 0 2px no-repeat;
}
.statistics .closed .stats-data {
display: none;
}
.stats-data td {
padding-right: 5px;
}
</style>
EOF
);
$oPage->add('<div id="XlsxExportDlg">');
$oPage->add('<div class="export-options">');
$oPage->add('<p><input type="checkbox" id="export-advanced-mode"/>&nbsp;<label for="export-advanced-mode">'.Dict::S('UI:CSVImport:AdvancedMode').'</label></p>');
$oPage->add('<p style="font-size:10pt;margin-left:2em;margin-top:-0.5em;padding-bottom:1em;">'.Dict::S('UI:CSVImport:AdvancedMode+').'</p>');
$oPage->add('<p><input type="checkbox" id="export-auto-download" checked="checked"/>&nbsp;<label for="export-auto-download">'.Dict::S('ExcelExport:AutoDownload').'</label></p>');
$oPage->add('</div>');
$oPage->add('<div class="progress"><p class="status-message">'.Dict::S('ExcelExport:PreparingExport').'</p><div class="progress-bar"><div class="progress-label"></div></div></div>');
$oPage->add('<div class="statistics"><div class="stats-toggle closed">'.Dict::S('ExcelExport:Statistics').'<div class="stats-data"></div></div></div>');
$oPage->add('</div>');
$aLabels = array(
'dialog_title' => Dict::S('ExcelExporter:ExportDialogTitle'),
'cancel_button' => Dict::S('UI:Button:Cancel'),
'export_button' => Dict::S('ExcelExporter:ExportButton'),
'download_button' => Dict::Format('ExcelExporter:DownloadButton', 'export.xlsx'), //TODO: better name for the file (based on the class of the filter??)
);
$sJSLabels = json_encode($aLabels);
$sFilter = addslashes($sFilter);
$sJSPageUrl = addslashes(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php');
$oPage->add_ready_script("$('#XlsxExportDlg').xlsxexporter({filter: '$sFilter', labels: $sJSLabels, ajax_page_url: '$sJSPageUrl'});");
break;
case 'xlsx_start':
$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
$bAdvanced = (utils::ReadParam('advanced', 'false') == 'true');
$oSearch = DBObjectSearch::unserialize($sFilter);
$oExcelExporter = new ExcelExporter();
$oExcelExporter->SetObjectList($oSearch);
//$oExcelExporter->SetChunkSize(10); //Only for testing
$oExcelExporter->SetAdvancedMode($bAdvanced);
$sToken = $oExcelExporter->SaveState();
$oPage->add(json_encode(array('status' => 'ok', 'token' => $sToken)));
break;
case 'xlsx_run':
$sMemoryLimit = MetaModel::GetConfig()->Get('xlsx_exporter_memory_limit');
ini_set('memory_limit', $sMemoryLimit);
ini_set('max_execution_time', max(300, ini_get('max_execution_time'))); // At least 5 minutes
$sToken = utils::ReadParam('token', '', false, 'raw_data');
$oExcelExporter = new ExcelExporter($sToken);
$aStatus = $oExcelExporter->Run();
$aResults = array('status' => $aStatus['code'], 'percentage' => $aStatus['percentage'], 'message' => $aStatus['message']);
if ($aStatus['code'] == 'done')
{
$aResults['statistics'] = $oExcelExporter->GetStatistics('html');
}
$oPage->add(json_encode($aResults));
break;
case 'xlsx_download':
$sToken = utils::ReadParam('token', '', false, 'raw_data');
$oPage->SetContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
$oPage->SetContentDisposition('attachment', 'export.xlsx');
$sFileContent = ExcelExporter::GetExcelFileFromToken($sToken);
$oPage->add($sFileContent);
ExcelExporter::CleanupFromToken($sToken);
break;
case 'xlsx_abort':
// Stop & cleanup an export...
$sToken = utils::ReadParam('token', '', false, 'raw_data');
ExcelExporter::CleanupFromToken($sToken);
break;
default:
$oPage->p("Invalid query.");
}

View File

@@ -60,8 +60,7 @@ function ValidateObject($oObject)
if (IsPowerUser())
{
$sValidationDefine = 'PORTAL_'.strtoupper(get_class($oObject)).'_DISPLAY_POWERUSER_QUERY';
}
else
} else
{
$sValidationDefine = 'PORTAL_'.strtoupper(get_class($oObject)).'_DISPLAY_QUERY';
}
@@ -162,10 +161,17 @@ function DisplayMainMenu(WebPage $oP)
$oP->AddMenuButton('showongoing', 'Portal:ShowOngoing', '../portal/index.php?operation=show_ongoing');
$oP->AddMenuButton('newrequest', 'Portal:CreateNewRequest', '../portal/index.php?operation=create_request');
$oP->AddMenuButton('showclosed', 'Portal:ShowClosed', '../portal/index.php?operation=show_closed');
$oP->AddMenuButton('showtoapprove', 'Portal:ShowToApprove', '../portal/index.php?operation=show_toapprove');
if (isKeyUser()) {
$oP->AddMenuButton('showtoresolve', 'Portal:ShowToResolve', '../portal/index.php?operation=show_toresolve');
}
/* THEBEN
if (UserRights::CanChangePassword())
{
$oP->AddMenuButton('change_pwd', 'Portal:ChangeMyPassword', '../portal/index.php?loginop=change_pwd');
}
} */
}
/**
@@ -184,6 +190,48 @@ function ShowOngoingTickets(WebPage $oP)
$oP->add("<h1 id=\"#resolved_requests\">".Dict::S('Portal:ResolvedRequests')."</h1>\n");
ListResolvedRequests($oP);
$oP->add("</div>\n");
$oP->add("<div id=\"#requests_to_approve\">\n");
$oP->add("<h1 id=\"#title_requests_to_approve\">".Dict::S('Portal:RequestsToApprove')."</h1>\n");
ListRequestsToApprove($oP);
$oP->add("</div>\n");
if (isKeyUser()) {
$oP->add("<div id=\"open_requests\">\n");
$oP->add("<h1 id=\"title_requests_to_resolve\">".Dict::S('Portal:RequestsToResolve')."</h1>\n");
ListRequestsToResolve($oP);
$oP->add("</div>\n");
}
}
/**
* Displays the tickets which need approval by mysel
* @param WebPage $oP The current web page
* @return void
*/
// =========================== THEBEN ==================================
function ShowTicketsToApprove(WebPage $oP)
{
$oP->add("<div id=\"open_requests\">\n");
$oP->add("<h1 id=\"title_open_requests\">".Dict::S('Portal:RequestsToApprove')."</h1>\n");
ListRequestsToApprove($oP);
$oP->add("</div>\n");
}
/**
* Displays the tickets which need approval by mysel
* @param WebPage $oP The current web page
* @return void
*/
// =========================== THEBEN ==================================
function ShowTicketsToResolve(WebPage $oP)
{
$oP->add("<div id=\"open_requests\">\n");
$oP->add("<h1 id=\"title_requests_to_resolve\">".Dict::S('Portal:RequestsToResolve')."</h1>\n");
ListRequestsToResolve($oP);
$oP->add("</div>\n");
}
/**
@@ -199,134 +247,6 @@ function ShowClosedTickets(WebPage $oP)
$oP->add("</div>\n");
}
/**
* Displays the form to select a Service Category Id (among the valid ones for the specified user Organization)
* @param WebPage $oP Web page for the form output
* @param Organization $oUserOrg The organization of the current user
* @return void
*/
function SelectServiceCategory($oP, $oUserOrg)
{
$aParameters = $oP->ReadAllParams(PORTAL_ALL_PARAMS.',template_id');
$oSearch = DBObjectSearch::FromOQL(PORTAL_SERVICECATEGORY_QUERY);
$oSearch->AllowAllData(); // In case the user has the rights on his org only
$oSet = new CMDBObjectSet($oSearch, array(), array('org_id' => $oUserOrg->GetKey()));
if ($oSet->Count() == 1)
{
$oService = $oSet->Fetch();
$iSvcCategory = $oService->GetKey();
// Only one Category, skip this step in the wizard
SelectServiceSubCategory($oP, $oUserOrg, $iSvcCategory);
}
else
{
$oP->add("<div class=\"wizContainer\" id=\"form_select_service\">\n");
$oP->WizardFormStart('request_wizard', 1);
$oP->add("<h1 id=\"select_category\">".Dict::S('Portal:SelectService')."</h1>\n");
$oP->add("<table>\n");
while($oService = $oSet->Fetch())
{
$id = $oService->GetKey();
$sChecked = "";
if (isset($aParameters['service_id']) && ($id == $aParameters['service_id']))
{
$sChecked = "checked";
}
$oP->p("<tr><td style=\"vertical-align:top\"><p><input name=\"attr_service_id\" $sChecked type=\"radio\" id=\"service_$id\" value=\"$id\"></p></td><td style=\"vertical-align:top\"><p><b><label for=\"service_$id\">".$oService->GetName()."</label></b></p>");
$oP->p("<p>".$oService->GetAsHTML('description')."</p></td></tr>");
}
$oP->add("</table>\n");
$oP->DumpHiddenParams($aParameters, array('service_id'));
$oP->add("<input type=\"hidden\" name=\"operation\" value=\"create_request\">");
$oP->WizardFormButtons(BUTTON_NEXT | BUTTON_CANCEL); // NO back button since it's the first step of the Wizard
$oP->WizardFormEnd();
$oP->WizardCheckSelectionOnSubmit(Dict::S('Portal:PleaseSelectOneService'));
$oP->add("</div>\n");
}
}
/**
* Displays the form to select a Service Subcategory Id (among the valid ones for the specified user Organization)
* and based on the page's parameter 'service_id'
* @param WebPage $oP Web page for the form output
* @param Organization $oUserOrg The organization of the current user
* @param $iSvcId Id of the selected service in case of pass-through (when there is only one service)
* @return void
*/
function SelectServiceSubCategory($oP, $oUserOrg, $iSvcId = null)
{
$aParameters = $oP->ReadAllParams(PORTAL_ALL_PARAMS.',template_id');
if ($iSvcId == null)
{
$iSvcId = $aParameters['service_id'];
}
else
{
$aParameters['service_id'] = $iSvcId;
}
$iDefaultSubSvcId = isset($aParameters['servicesubcategory_id']) ? $aParameters['servicesubcategory_id'] : 0;
$iDefaultWizNext = 2;
$oSearch = DBObjectSearch::FromOQL(PORTAL_SERVICE_SUBCATEGORY_QUERY);
RestrictSubcategories($oSearch);
$oSearch->AllowAllData(); // In case the user has the rights on his org only
$oSet = new CMDBObjectSet($oSearch, array(), array('svc_id' => $iSvcId, 'org_id' => $oUserOrg->GetKey()));
if ($oSet->Count() == 1)
{
// Only one sub service, skip this step of the wizard
$oSubService = $oSet->Fetch();
$iSubSvdId = $oSubService->GetKey();
SelectRequestTemplate($oP, $oUserOrg, $iSvcId, $iSubSvdId);
}
else
{
$oServiceCategory = MetaModel::GetObject('Service', $iSvcId, false, true /* allow all data*/);
if (is_object($oServiceCategory))
{
$oP->add("<div class=\"wizContainer\" id=\"form_select_servicesubcategory\">\n");
$oP->add("<h1 id=\"select_subcategory\">".Dict::Format('Portal:SelectSubcategoryFrom_Service', $oServiceCategory->GetName())."</h1>\n");
$oP->WizardFormStart('request_wizard', $iDefaultWizNext);
$oP->add("<table>\n");
while($oSubService = $oSet->Fetch())
{
$id = $oSubService->GetKey();
$sChecked = "";
if ($id == $iDefaultSubSvcId)
{
$sChecked = "checked";
}
$oP->add("<tr>");
$oP->add("<td style=\"vertical-align:top\">");
$oP->add("<p><input name=\"attr_servicesubcategory_id\" $sChecked type=\"radio\" id=\"servicesubcategory_$id\" value=\"$id\"></p>");
$oP->add("</td>");
$oP->add("<td style=\"vertical-align:top\">");
$oP->add("<p><b><label for=\"servicesubcategory_$id\">".$oSubService->GetName()."</label></b></p>");
$oP->add("<p>".$oSubService->GetAsHTML('description')."</p>");
$oP->add("</td>");
$oP->add("</tr>");
}
$oP->add("</table>\n");
$oP->DumpHiddenParams($aParameters, array('servicesubcategory_id'));
$oP->add("<input type=\"hidden\" name=\"operation\" value=\"create_request\">");
$oP->WizardFormButtons(BUTTON_BACK | BUTTON_NEXT | BUTTON_CANCEL); //Back button automatically discarded if on the first page
$oP->WizardFormEnd();
$oP->WizardCheckSelectionOnSubmit(Dict::S('Portal:PleaseSelectAServiceSubCategory'));
$oP->add("</div>\n");
}
else
{
$oP->p("Error: Invalid Service: id = $iSvcId");
}
}
}
/**
* Displays the form to select a Template
* @param WebPage $oP Web page for the form output
@@ -437,21 +357,9 @@ function SelectRequestTemplate($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null
* @param integer $iTemplateId The identifier of the template (fall through when there is only one template)
* @return void
*/
function RequestCreationForm($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null, $iTemplateId = null)
function RequestCreationForm($oP, $oUserOrg)
{
$aParameters = $oP->ReadAllParams(PORTAL_ALL_PARAMS.',template_id');
if (!is_null($iSvcId))
{
$aParameters['service_id'] = $iSvcId;
}
if (!is_null($iSubSvcId))
{
$aParameters['servicesubcategory_id'] = $iSubSvcId;
}
if (!is_null($iTemplateId))
{
$aParameters['template_id'] = $iTemplateId;
}
$sDescription = '';
if (isset($aParameters['template_id']) && ($aParameters['template_id'] != 0))
@@ -475,25 +383,33 @@ function RequestCreationForm($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null,
}
}
$oServiceCategory = MetaModel::GetObject('Service', $aParameters['service_id'], false, true /* allow all data*/);
$oServiceSubCategory = MetaModel::GetObject('ServiceSubcategory', $aParameters['servicesubcategory_id'], false, true /* allow all data*/);
if (is_object($oServiceCategory) && is_object($oServiceSubCategory))
//$oServiceCategory = MetaModel::GetObject('Service', $aParameters['service_id'], false, true /* allow all data*/);
//$oServiceSubCategory = MetaModel::GetObject('ServiceSubcategory', $aParameters['servicesubcategory_id'], false, true /* allow all data*/);
//if (is_object($oServiceCategory) && is_object($oServiceSubCategory))
{
$sClass = ComputeClass($oServiceSubCategory->GetKey());
$sClass = "UserRequest"; // ComputeClass($oServiceSubCategory->GetKey());
$oRequest = MetaModel::NewObject($sClass);
$oRequest->Set('org_id', $oUserOrg->GetKey());
$oRequest->Set('caller_id', UserRights::GetContactId());
$oRequest->Set('service_id', $aParameters['service_id']);
$oRequest->Set('servicesubcategory_id', $aParameters['servicesubcategory_id']);
// $oRequest->Set('service_id', $aParameters['service_id']);
// $oRequest->Set('servicesubcategory_id', $aParameters['servicesubcategory_id']);
$oAttDef = MetaModel::GetAttributeDef($sClass, 'service_id');
/* $oAttDef = MetaModel::GetAttributeDef($sClass, 'service_id');
$aDetails[] = array('label' => $oAttDef->GetLabel(), 'value' => $oServiceCategory->GetName());
$oAttDef = MetaModel::GetAttributeDef($sClass, 'servicesubcategory_id');
$aDetails[] = array('label' => $oAttDef->GetLabel(), 'value' => $oServiceSubCategory->GetName());
*/
$aList = explode(',', GetConstant($sClass, 'FORM_ATTRIBUTES'));
IssueLog::info("aList, FORM_ATTRIBUTES=".print_r($aList,true));
// TODO
/* echo "<select name='userCharacters' id='userCharacter'>";
echo "<option value='1'>Kid Wonder</option>";
echo "<option value='3'>Oriel</option>";
echo "</select>"; */
$iFlags = 0;
foreach($aList as $sAttCode)
{
@@ -507,8 +423,10 @@ function RequestCreationForm($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null,
$aFieldsMap = array();
foreach($aList as $sAttCode)
{
IssueLog::Info("sAttCode=".$sAttCode);
$value = '';
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
IssueLog::Info("oAttDef=".print_r($oAttDef,true));
$iFlags = $oRequest->GetAttributeFlags($sAttCode);
if (isset($aParameters[$sAttCode]))
{
@@ -517,8 +435,10 @@ function RequestCreationForm($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null,
$aArgs = array('this' => $oRequest);
$sInputId = 'attr_'.$sAttCode;
IssueLog::Info("sInputId=".$sInputId);
$aFieldsMap[$sAttCode] = $sInputId;
$sValue = "<span id=\"field_{$sInputId}\">".$oRequest->GetFormElementForField($oP, $sClass, $sAttCode, $oAttDef, $value, '', 'attr_'.$sAttCode, '', $iFlags, $aArgs).'</span>';
IssueLog::Info("sValue=".$sValue);
$aDetails[] = array('label' => $oAttDef->GetLabel(), 'value' => $sValue);
}
$aHidden = array();
@@ -527,6 +447,7 @@ function RequestCreationForm($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null,
foreach ($aTemplateFields as $sAttCode => $oField)
{
$sValue = $oField->GetFormElement($oP, $sClass);
if ($oField->Get('input_type') == 'hidden')
{
$aHidden[] = $sValue;
@@ -554,7 +475,7 @@ EOF
$oP->add_linked_script("../js/jquery.blockUI.js");
$oP->add("<div class=\"wizContainer\" id=\"form_request_description\">\n");
$oP->add("<h1 id=\"title_request_form\">".Dict::S('Portal:DescriptionOfTheRequest')."</h1>\n");
$oP->WizardFormStart('request_form', 4);
$oP->WizardFormStart('request_form', 1);
$oP->details($aDetails);
@@ -587,7 +508,7 @@ EOF
$oP->add("</div>\n");
$iFieldsCount = count($aFieldsMap);
$sJsonFieldsMap = json_encode($aFieldsMap);
// IssueLog::Info("sJsonFieldsMap=".$sJsonFieldsMap);
$oP->add_ready_script(
<<<EOF
oWizardHelper.SetFieldsMap($sJsonFieldsMap);
@@ -601,11 +522,11 @@ EOF
EOF
);
}
else
{
// else
// {
// User not authorized to use this service ?
//ShowOngoingTickets($oP);
}
// }
}
/**
@@ -615,7 +536,7 @@ EOF
* @return void
*/
function DoCreateRequest($oP, $oUserOrg)
{
{ IssueLog::Info("DoCreateRequest");
$aParameters = $oP->ReadAllParams(PORTAL_ALL_PARAMS.',template_id');
$sTransactionId = utils::ReadPostedParam('transaction_id', '');
if (!utils::IsTransactionValid($sTransactionId))
@@ -713,8 +634,8 @@ function CreateRequest(WebPage $oP, Organization $oUserOrg)
{
switch($oP->GetWizardStep())
{
case 0:
default:
// case 0:
/* default:
SelectServiceCategory($oP, $oUserOrg);
break;
@@ -724,13 +645,13 @@ function CreateRequest(WebPage $oP, Organization $oUserOrg)
case 2:
SelectRequestTemplate($oP, $oUserOrg);
break;
break; */
case 3:
case 0:
RequestCreationForm($oP, $oUserOrg);
break;
case 4:
case 1:
DoCreateRequest($oP, $oUserOrg);
break;
}
@@ -825,6 +746,54 @@ function ListResolvedRequests(WebPage $oP)
DisplayRequestLists($oP, $aClassToSet);
}
/**
* Lists all the currently resolved (not yet closed) User Requests for the current user
* @param WebPage $oP The current web page
* @return void
*/
// ========================= THEBEN ===================================
function ListRequestsToApprove(WebPage $oP)
{
$oUserOrg = GetUserOrg();
$aClassToSet = array();
foreach (GetTicketClasses() as $sClass)
{
$sOQL = "SELECT $sClass WHERE org_id = :org_id AND status = 'waiting_for_approval'";
$oSearch = DBObjectSearch::FromOQL($sOQL);
$iUser = UserRights::GetContactId();
$oSearch->AddCondition('approver_id', $iUser);
$aClassToSet[$sClass] = new CMDBObjectSet($oSearch, array(), array('org_id' => $oUserOrg->GetKey()));
}
DisplayRequestLists($oP, $aClassToSet);
}
/**
* Lists all the currently resolved (not yet closed) User Requests for the current user
* @param WebPage $oP The current web page
* @return void
*/
// ========================= THEBEN ===================================
function ListRequestsToResolve(WebPage $oP)
{
$oUserOrg = GetUserOrg();
$aClassToSet = array();
foreach (GetTicketClasses() as $sClass)
{
$sOQL = "SELECT $sClass WHERE org_id = :org_id AND status = 'assigned'";
$oSearch = DBObjectSearch::FromOQL($sOQL);
$iUser = UserRights::GetContactId();
$oSearch->AddCondition('agent_id', $iUser);
$aClassToSet[$sClass] = new CMDBObjectSet($oSearch, array(), array('org_id' => $oUserOrg->GetKey()));
}
DisplayRequestLists($oP, $aClassToSet);
}
/**
* Lists all the currently closed tickets
* @param WebPage $oP The current web page
@@ -896,6 +865,8 @@ function DisplayObject($oP, $oObj, $oUserOrg)
* @param Object $oObj The target object
* @return void
*/
//============= THEBEN ============= ApproveButton === ResolveButton ===============
function ShowDetailsRequest(WebPage $oP, $oObj)
{
$sClass = get_class($oObj);
@@ -905,6 +876,10 @@ function ShowDetailsRequest(WebPage $oP, $oObj)
$bIsReopenButton = false;
$bIsCloseButton = false;
$bIsEscalateButton = false;
$bIsApproveButton = false;
$bIsRejectButton = false;
$bIsResolveButton = false;
$bEditAttachments = false;
$aEditAtt = array(); // List of attributes editable in the main form
if (!MetaModel::DBIsReadOnly())
@@ -923,16 +898,66 @@ function ShowDetailsRequest(WebPage $oP, $oObj)
}
// Add the "Close" button if this is valid action
if (array_key_exists('ev_close', $aTransitions) && UserRights::IsStimulusAllowed($sClass, 'ev_close', $oSet))
{
{ if (($oObj->Get('caller_id') == UserRights::GetContactId())
|| IsPowerUSer()) {
// I can only close a ticket if I'm the caller or a power user
$bIsCloseButton = true;
MakeStimulusForm($oP, $oObj, 'ev_close', array('user_satisfaction', $sUserCommentAttCode));
}
IssueLog::Info('caller_id='.$oObj->Get('caller_id')." user-id=".UserRights::GetContactId());
}
break;
case 'waiting_for_approval':
$aEditAtt = array();
$aTransitions = $oObj->EnumTransitions();
$oSet = DBObjectSet::FromObject($oObj);
// Add the "Approve" button if this is valid action
if (array_key_exists('ev_approve', $aTransitions)) //TODO check if current user is approver && UserRights::IsStimulusAllowed($sClass, 'ev_reopen', $oSet))
{
$bIsApproveButton = true;
MakeStimulusForm($oP, $oObj, 'ev_approve', array($sLogAttCode));
}
// Add the "Reject" button if this is valid action
if (array_key_exists('ev_reject', $aTransitions)) // TODO check if current user is approve && UserRights::IsStimulusAllowed($sClass, 'ev_close', $oSet))
{
$bIsRejectButton = true;
MakeStimulusForm($oP, $oObj, 'ev_reject', array($sLogAttCode) ); //array('user_satisfaction', $sUserCommentAttCode));
}
break;
case 'closed':
// By convention 'closed' is the final state of a ticket and nothing can be done in such a state
break;
case 'assigned':
$iFlags = $oObj->GetAttributeFlags($sLogAttCode);
$bReadOnly = (($iFlags & (OPT_ATT_READONLY | OPT_ATT_HIDDEN)) != 0);
if ($bReadOnly)
{
$aEditAtt = array();
$bEditAttachments = false;
}
else
{
$aEditAtt = array(
$sLogAttCode => '????'
);
$bEditAttachments = true;
}
if (isKeyUser()) {
$aTransitions = $oObj->EnumTransitions();
$oSet = DBObjectSet::FromObject($oObj);
// Add the "Resolver" button if this is valid action
if (array_key_exists('ev_resolve', $aTransitions)) //TODO check if current user is approver && UserRights::IsStimulusAllowed($sClass, 'ev_reopen', $oSet))
{
$bIsResolveButton = true;
MakeStimulusForm($oP, $oObj, 'ev_resolve', array($sLogAttCode));
}
}
break;
default:
// In all other states, the only possible action is to update the ticket (both the case log and the attachments)
// This update is possible only if the case log field is not read-only or hidden in the current state
@@ -1073,6 +1098,32 @@ EOF
$sOk = addslashes(Dict::S('UI:Button:Ok'));
$oP->p('<input type="button" onClick="RunStimulusDialog(\''.$sStimulusCode.'\', \''.$sTitle.'\', \''.$sOk.'\');" value="'.$sTitle.'...">');
}
if($bIsApproveButton)
{
$sStimulusCode = 'ev_approve';
$sTitle = addslashes(Dict::S('Portal:Button:ApproveTicket'));
$sOk = addslashes(Dict::S('UI:Button:Ok'));
$oP->p('<input type="button" onClick="RunStimulusDialog(\''.$sStimulusCode.'\', \''.$sTitle.'\', \''.$sOk.'\');" value="'.$sTitle.'...">');
}
if($bIsRejectButton)
{
$sStimulusCode = 'ev_reject';
$sTitle = addslashes(Dict::S('Portal:Button:RejectTicket'));
$sOk = addslashes(Dict::S('UI:Button:Ok'));
$oP->p('<input type="button" onClick="RunStimulusDialog(\''.$sStimulusCode.'\', \''.$sTitle.'\', \''.$sOk.'\');" value="'.$sTitle.'...">');
}
if($bIsResolveButton)
{
$sStimulusCode = 'ev_resolve';
$sTitle = addslashes(Dict::S('Portal:Button:ResolveTicket'));
$sOk = addslashes(Dict::S('UI:Button:Ok'));
$oP->p('<input type="button" onClick="RunStimulusDialog(\''.$sStimulusCode.'\', \''.$sTitle.'\', \''.$sOk.'\');" value="'.$sTitle.'...">');
}
if($bIsCloseButton)
{
$sStimulusCode = 'ev_close';
@@ -1257,6 +1308,26 @@ function IsPowerUSer()
return $bRes;
}
/**
* Determine if the current user can be considered as being a portal key user
* (can update tickets where he is agent and can resolve them)
*/
function IsKeyUSer()
{
$iUserID = UserRights::GetUserId();
$sOQLprofile = "SELECT URP_Profiles AS p JOIN URP_UserProfile AS up ON up.profileid=p.id WHERE up.userid = :user AND p.name = :profile";
$oProfileSet = new DBObjectSet(
DBObjectSearch::FromOQL($sOQLprofile),
array(),
array(
'user' => $iUserID,
'profile' => 'PORTAL_KEY_USER_PROFILE',
)
);
$bRes = ($oProfileSet->count() > 0);
return $bRes;
}
///////////////////////////////////////////////////////////////////////////////
//
// Main program
@@ -1288,9 +1359,11 @@ try
$sCode = $oUserOrg->Get('code');
$sAlternateStylesheet = '';
//IssueLog::Info("org code of user=".$sCode);
if (@file_exists("./$sCode/portal.css"))
{
$sAlternateStylesheet = "$sCode";
IssueLog::Info("using Alt Stylesheet: ".$sAlternateStylesheet);
}
$oP = new PortalWebPage(Dict::S('Portal:Title'), $sAlternateStylesheet);
@@ -1350,11 +1423,26 @@ try
}
break;
// =================== THEBEN ===========================
case 'show_toapprove':
$oP->set_title(Dict::S('Portal:ShowToApprove'));
DisplayMainMenu($oP);
ShowTicketsToApprove($oP);
break;
// =================== THEBEN ===========================
case 'show_toresolve':
$oP->set_title(Dict::S('Portal:ShowToResolve'));
DisplayMainMenu($oP);
ShowTicketsToResolve($oP);
break;
case 'show_ongoing':
default:
$oP->set_title(Dict::S('Portal:ShowOngoing'));
DisplayMainMenu($oP);
ShowOngoingTickets($oP);
break;
}
}
}

View File

@@ -803,5 +803,30 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</pre>
]]></text>
</license>
<license>
<product>PHP XLSXWriter</product>
<author>Mark Jones</author>
<license_type>MIT</license_type>
<text><![CDATA[
<pre>Copyright (c) 2013 Mark Jones
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</pre>
]]></text>
</license>
</licenses>

View File

@@ -28,9 +28,11 @@ if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__));
require_once(__DIR__.'/../approot.inc.php');
require_once(APPROOT.'/application/application.inc.php');
require_once(APPROOT.'/application/nicewebpage.class.inc.php');
require_once(APPROOT.'/application/ajaxwebpage.class.inc.php');
require_once(APPROOT.'/application/csvpage.class.inc.php');
require_once(APPROOT.'/application/xmlpage.class.inc.php');
require_once(APPROOT.'/application/clipage.class.inc.php');
require_once(APPROOT.'/application/excelexporter.class.inc.php');
require_once(APPROOT.'/application/startup.inc.php');
@@ -264,6 +266,32 @@ if (!empty($sExpression))
cmdbAbstractObject::DisplaySetAsXML($oP, $oSet, array('localize_values' => $bLocalize));
break;
case 'xlsx':
$oP = new ajax_page('');
$oExporter = new ExcelExporter();
$oExporter->SetObjectList($oFilter);
// Run the export by chunk of 1000 objects to limit memory usage
$oExporter->SetChunkSize(1000);
do
{
$aStatus = $oExporter->Run(); // process one chunk
}
while( ($aStatus['code'] != 'done') && ($aStatus['code'] != 'error'));
if ($aStatus['code'] == 'done')
{
$oP->SetContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
$oP->SetContentDisposition('attachment', $oFilter->GetClass().'.xlsx');
$oP->add(file_get_contents($oExporter->GetExcelFilePath()));
$oExporter->Cleanup();
}
else
{
$oP->add('Error, xlsx export failed: '.$aStatus['message']);
}
break;
default:
$oP = new WebPage("iTop - Export");
$oP->add("Unsupported format '$sFormat'. Possible values are: html, csv, spreadsheet or xml.");
@@ -301,7 +329,7 @@ if (!$oP)
$oP->p(" * expression: an OQL expression (URL encoded if needed)");
$oP->p(" * query: (alternative to 'expression') the id of an entry from the query phrasebook");
$oP->p(" * arg_xxx: (needed if the query has parameters) the value of the parameter 'xxx'");
$oP->p(" * format: (optional, default is html) the desired output format. Can be one of 'html', 'spreadsheet', 'csv' or 'xml'");
$oP->p(" * format: (optional, default is html) the desired output format. Can be one of 'html', 'spreadsheet', 'csv', 'xlsx' or 'xml'");
$oP->p(" * fields: (optional, no effect on XML format) list of fields (attribute codes, or alias.attcode) separated by a coma");
$oP->p(" * fields_advanced: (optional, no effect on XML/HTML formats ; ignored is fields is specified) If set to 1, the default list of fields will include the external keys and their reconciliation keys");
$oP->p(" * filename: (optional, no effect in CLI mode) if set then the results will be downloaded as a file");