From 0409279595464924d1dc728a8c6830228d75dc5f Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 29 Jan 2018 06:50:11 -0800 Subject: [PATCH] Support Excel as a data export format Summary: Depends on D18954. Ref T13049. This brings over the existing Maniphest Excel export pipeline in a generic way. The `ExportField` classes know directly that `PHPExcel` exists, which is a little sketchy, but writing an Excel indirection layer sounds like a lot of work and I don't anticipate us changing Excel backends anytime soon, so trying to abstract this feels YAGNI. This doesn't bring over the install instructions for PHPExcel or the detection of whether or not it exists. I'll bring that over in a future change. Test Plan: Exported users as Excel, opened them up, got a sensible-looking Excel sheet. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13049 Differential Revision: https://secure.phabricator.com/D18955 --- src/__phutil_library_map__.php | 2 + ...PhabricatorApplicationSearchController.php | 7 +- .../export/PhabricatorEpochExportField.php | 20 +++ .../export/PhabricatorExcelExportFormat.php | 145 ++++++++++++++++++ .../export/PhabricatorExportField.php | 15 ++ .../export/PhabricatorExportFormat.php | 10 ++ .../export/PhabricatorIDExportField.php | 4 + .../export/PhabricatorIntExportField.php | 15 ++ .../export/PhabricatorPHIDExportField.php | 8 +- 9 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 src/infrastructure/export/PhabricatorExcelExportFormat.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 03ac41fe7f..3ef9f4ec3b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2847,6 +2847,7 @@ phutil_register_library_map(array( 'PhabricatorEventListener' => 'infrastructure/events/PhabricatorEventListener.php', 'PhabricatorEventType' => 'infrastructure/events/constant/PhabricatorEventType.php', 'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php', + 'PhabricatorExcelExportFormat' => 'infrastructure/export/PhabricatorExcelExportFormat.php', 'PhabricatorExecFutureFileUploadSource' => 'applications/files/uploadsource/PhabricatorExecFutureFileUploadSource.php', 'PhabricatorExportEngineExtension' => 'infrastructure/export/PhabricatorExportEngineExtension.php', 'PhabricatorExportField' => 'infrastructure/export/PhabricatorExportField.php', @@ -8282,6 +8283,7 @@ phutil_register_library_map(array( 'PhabricatorEventListener' => 'PhutilEventListener', 'PhabricatorEventType' => 'PhutilEventType', 'PhabricatorExampleEventListener' => 'PhabricatorEventListener', + 'PhabricatorExcelExportFormat' => 'PhabricatorExportFormat', 'PhabricatorExecFutureFileUploadSource' => 'PhabricatorFileUploadSource', 'PhabricatorExportEngineExtension' => 'Phobject', 'PhabricatorExportField' => 'Phobject', diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index e980498c42..88cfd3c137 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -410,8 +410,10 @@ final class PhabricatorApplicationSearchController if ($named_query) { $filename = $named_query->getQueryName(); + $sheet_title = $named_query->getQueryName(); } else { $filename = $engine->getResultTypeDescription(); + $sheet_title = $engine->getResultTypeDescription(); } $filename = phutil_utf8_strtolower($filename); $filename = PhabricatorFile::normalizeFileName($filename); @@ -445,8 +447,9 @@ final class PhabricatorApplicationSearchController $mime_type = $format->getMIMEContentType(); $filename = $filename.'.'.$extension; - $format = clone $format; - $format->setViewer($viewer); + $format = id(clone $format) + ->setViewer($viewer) + ->setTitle($sheet_title); $export_data = $engine->newExport($objects); $objects = array_values($objects); diff --git a/src/infrastructure/export/PhabricatorEpochExportField.php b/src/infrastructure/export/PhabricatorEpochExportField.php index a19e60b50e..4dffde5aa8 100644 --- a/src/infrastructure/export/PhabricatorEpochExportField.php +++ b/src/infrastructure/export/PhabricatorEpochExportField.php @@ -24,4 +24,24 @@ final class PhabricatorEpochExportField return (int)$value; } + public function getPHPExcelValue($value) { + $epoch = $this->getNaturalValue($value); + + $seconds_per_day = phutil_units('1 day in seconds'); + $offset = ($seconds_per_day * 25569); + + return ($epoch + $offset) / $seconds_per_day; + } + + /** + * @phutil-external-symbol class PHPExcel_Style_NumberFormat + */ + public function formatPHPExcelCell($cell, $style) { + $code = PHPExcel_Style_NumberFormat::FORMAT_DATE_YYYYMMDD2; + + $style + ->getNumberFormat() + ->setFormatCode($code); + } + } diff --git a/src/infrastructure/export/PhabricatorExcelExportFormat.php b/src/infrastructure/export/PhabricatorExcelExportFormat.php new file mode 100644 index 0000000000..633d98fa53 --- /dev/null +++ b/src/infrastructure/export/PhabricatorExcelExportFormat.php @@ -0,0 +1,145 @@ +getSheet(); + + $header_format = array( + 'font' => array( + 'bold' => true, + ), + ); + + $row = 1; + $col = 0; + foreach ($fields as $field) { + $cell_value = $field->getLabel(); + + $cell_name = $this->getCellName($col, $row); + + $cell = $sheet->setCellValue( + $cell_name, + $cell_value, + $return_cell = true); + + $sheet->getStyle($cell_name)->applyFromArray($header_format); + $cell->setDataType(PHPExcel_Cell_DataType::TYPE_STRING); + + $width = $field->getCharacterWidth(); + if ($width !== null) { + $col_name = $this->getCellName($col); + $sheet->getColumnDimension($col_name) + ->setWidth($width); + } + + $col++; + } + } + + public function addObject($object, array $fields, array $map) { + $sheet = $this->getSheet(); + + $col = 0; + foreach ($fields as $key => $field) { + $cell_value = $map[$key]; + $cell_value = $field->getPHPExcelValue($cell_value); + + $cell_name = $this->getCellName($col, $this->rowCursor); + + $cell = $sheet->setCellValue( + $cell_name, + $cell_value, + $return_cell = true); + + $style = $sheet->getStyle($cell_name); + $field->formatPHPExcelCell($cell, $style); + + $col++; + } + + $this->rowCursor++; + } + + /** + * @phutil-external-symbol class PHPExcel_IOFactory + */ + public function newFileData() { + $workbook = $this->getWorkbook(); + $writer = PHPExcel_IOFactory::createWriter($workbook, 'Excel2007'); + + ob_start(); + $writer->save('php://output'); + $data = ob_get_clean(); + + return $data; + } + + private function getWorkbook() { + if (!$this->workbook) { + $this->workbook = $this->newWorkbook(); + } + return $this->workbook; + } + + /** + * @phutil-external-symbol class PHPExcel + */ + private function newWorkbook() { + include_once 'PHPExcel.php'; + return new PHPExcel(); + } + + private function getSheet() { + if (!$this->sheet) { + $workbook = $this->getWorkbook(); + + $sheet = $workbook->setActiveSheetIndex(0); + $sheet->setTitle($this->getTitle()); + + $this->sheet = $sheet; + + // The row cursor starts on the second row, after the header row. + $this->rowCursor = 2; + } + + return $this->sheet; + } + + private function getCellName($col, $row = null) { + $col_name = chr(ord('A') + $col); + + if ($row === null) { + return $col_name; + } + + return $col_name.$row; + } + +} diff --git a/src/infrastructure/export/PhabricatorExportField.php b/src/infrastructure/export/PhabricatorExportField.php index 3efb7a8b9a..85e21b3e37 100644 --- a/src/infrastructure/export/PhabricatorExportField.php +++ b/src/infrastructure/export/PhabricatorExportField.php @@ -32,4 +32,19 @@ abstract class PhabricatorExportField return $value; } + public function getPHPExcelValue($value) { + return $this->getTextValue($value); + } + + /** + * @phutil-external-symbol class PHPExcel_Cell_DataType + */ + public function formatPHPExcelCell($cell, $style) { + $cell->setDataType(PHPExcel_Cell_DataType::TYPE_STRING); + } + + public function getCharacterWidth() { + return 24; + } + } diff --git a/src/infrastructure/export/PhabricatorExportFormat.php b/src/infrastructure/export/PhabricatorExportFormat.php index 9a8e035c58..7e174f5197 100644 --- a/src/infrastructure/export/PhabricatorExportFormat.php +++ b/src/infrastructure/export/PhabricatorExportFormat.php @@ -4,6 +4,7 @@ abstract class PhabricatorExportFormat extends Phobject { private $viewer; + private $title; final public function getExportFormatKey() { return $this->getPhobjectClassConstant('EXPORTKEY'); @@ -18,6 +19,15 @@ abstract class PhabricatorExportFormat return $this->viewer; } + final public function setTitle($title) { + $this->title = $title; + return $this; + } + + final public function getTitle() { + return $this->title; + } + abstract public function getExportFormatName(); abstract public function getMIMEContentType(); abstract public function getFileExtension(); diff --git a/src/infrastructure/export/PhabricatorIDExportField.php b/src/infrastructure/export/PhabricatorIDExportField.php index 5b29fdb21d..1ef3d53370 100644 --- a/src/infrastructure/export/PhabricatorIDExportField.php +++ b/src/infrastructure/export/PhabricatorIDExportField.php @@ -7,4 +7,8 @@ final class PhabricatorIDExportField return (int)$value; } + public function getCharacterWidth() { + return 12; + } + } diff --git a/src/infrastructure/export/PhabricatorIntExportField.php b/src/infrastructure/export/PhabricatorIntExportField.php index 3363f9b5d5..57f7e0ab29 100644 --- a/src/infrastructure/export/PhabricatorIntExportField.php +++ b/src/infrastructure/export/PhabricatorIntExportField.php @@ -4,7 +4,22 @@ final class PhabricatorIntExportField extends PhabricatorExportField { public function getNaturalValue($value) { + if ($value === null) { + return $value; + } + return (int)$value; } + /** + * @phutil-external-symbol class PHPExcel_Cell_DataType + */ + public function formatPHPExcelCell($cell, $style) { + $cell->setDataType(PHPExcel_Cell_DataType::TYPE_NUMERIC); + } + + public function getCharacterWidth() { + return 8; + } + } diff --git a/src/infrastructure/export/PhabricatorPHIDExportField.php b/src/infrastructure/export/PhabricatorPHIDExportField.php index 7c08ae0226..052c73fbd6 100644 --- a/src/infrastructure/export/PhabricatorPHIDExportField.php +++ b/src/infrastructure/export/PhabricatorPHIDExportField.php @@ -1,4 +1,10 @@