mirror of
https://we.phorge.it/source/phorge.git
synced 2025-03-24 18:20:14 +01:00
Support CSV, JSON, and tab-separated text as export formats
Summary: Depends on D18919. Ref T13046. Adds some simple modular exporters. Test Plan: Exported pull logs in each format. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13046 Differential Revision: https://secure.phabricator.com/D18934
This commit is contained in:
parent
c0b8e4784b
commit
a79bb55f3f
12 changed files with 319 additions and 37 deletions
|
@ -2231,6 +2231,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorBulkEngine' => 'applications/transactions/bulk/PhabricatorBulkEngine.php',
|
||||
'PhabricatorBulkManagementMakeSilentWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementMakeSilentWorkflow.php',
|
||||
'PhabricatorBulkManagementWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementWorkflow.php',
|
||||
'PhabricatorCSVExportFormat' => 'infrastructure/export/PhabricatorCSVExportFormat.php',
|
||||
'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php',
|
||||
'PhabricatorCacheEngine' => 'applications/system/engine/PhabricatorCacheEngine.php',
|
||||
'PhabricatorCacheEngineExtension' => 'applications/system/engine/PhabricatorCacheEngineExtension.php',
|
||||
|
@ -2844,6 +2845,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php',
|
||||
'PhabricatorExecFutureFileUploadSource' => 'applications/files/uploadsource/PhabricatorExecFutureFileUploadSource.php',
|
||||
'PhabricatorExportField' => 'infrastructure/export/PhabricatorExportField.php',
|
||||
'PhabricatorExportFormat' => 'infrastructure/export/PhabricatorExportFormat.php',
|
||||
'PhabricatorExtendedPolicyInterface' => 'applications/policy/interface/PhabricatorExtendedPolicyInterface.php',
|
||||
'PhabricatorExtendingPhabricatorConfigOptions' => 'applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php',
|
||||
'PhabricatorExtensionsSetupCheck' => 'applications/config/check/PhabricatorExtensionsSetupCheck.php',
|
||||
|
@ -3087,6 +3089,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php',
|
||||
'PhabricatorInstructionsEditField' => 'applications/transactions/editfield/PhabricatorInstructionsEditField.php',
|
||||
'PhabricatorIntConfigType' => 'applications/config/type/PhabricatorIntConfigType.php',
|
||||
'PhabricatorIntExportField' => 'infrastructure/export/PhabricatorIntExportField.php',
|
||||
'PhabricatorInternalSetting' => 'applications/settings/setting/PhabricatorInternalSetting.php',
|
||||
'PhabricatorInternationalizationManagementExtractWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php',
|
||||
'PhabricatorInternationalizationManagementWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementWorkflow.php',
|
||||
|
@ -3096,6 +3099,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorIteratorFileUploadSource' => 'applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php',
|
||||
'PhabricatorJIRAAuthProvider' => 'applications/auth/provider/PhabricatorJIRAAuthProvider.php',
|
||||
'PhabricatorJSONConfigType' => 'applications/config/type/PhabricatorJSONConfigType.php',
|
||||
'PhabricatorJSONExportFormat' => 'infrastructure/export/PhabricatorJSONExportFormat.php',
|
||||
'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php',
|
||||
'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php',
|
||||
'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.php',
|
||||
|
@ -4245,6 +4249,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorTextAreaEditField' => 'applications/transactions/editfield/PhabricatorTextAreaEditField.php',
|
||||
'PhabricatorTextConfigType' => 'applications/config/type/PhabricatorTextConfigType.php',
|
||||
'PhabricatorTextEditField' => 'applications/transactions/editfield/PhabricatorTextEditField.php',
|
||||
'PhabricatorTextExportFormat' => 'infrastructure/export/PhabricatorTextExportFormat.php',
|
||||
'PhabricatorTextListConfigType' => 'applications/config/type/PhabricatorTextListConfigType.php',
|
||||
'PhabricatorTime' => 'infrastructure/time/PhabricatorTime.php',
|
||||
'PhabricatorTimeFormatSetting' => 'applications/settings/setting/PhabricatorTimeFormatSetting.php',
|
||||
|
@ -7564,6 +7569,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorBulkEngine' => 'Phobject',
|
||||
'PhabricatorBulkManagementMakeSilentWorkflow' => 'PhabricatorBulkManagementWorkflow',
|
||||
'PhabricatorBulkManagementWorkflow' => 'PhabricatorManagementWorkflow',
|
||||
'PhabricatorCSVExportFormat' => 'PhabricatorExportFormat',
|
||||
'PhabricatorCacheDAO' => 'PhabricatorLiskDAO',
|
||||
'PhabricatorCacheEngine' => 'Phobject',
|
||||
'PhabricatorCacheEngineExtension' => 'Phobject',
|
||||
|
@ -8268,6 +8274,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorExampleEventListener' => 'PhabricatorEventListener',
|
||||
'PhabricatorExecFutureFileUploadSource' => 'PhabricatorFileUploadSource',
|
||||
'PhabricatorExportField' => 'Phobject',
|
||||
'PhabricatorExportFormat' => 'Phobject',
|
||||
'PhabricatorExtendingPhabricatorConfigOptions' => 'PhabricatorApplicationConfigOptions',
|
||||
'PhabricatorExtensionsSetupCheck' => 'PhabricatorSetupCheck',
|
||||
'PhabricatorExternalAccount' => array(
|
||||
|
@ -8551,6 +8558,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorInlineSummaryView' => 'AphrontView',
|
||||
'PhabricatorInstructionsEditField' => 'PhabricatorEditField',
|
||||
'PhabricatorIntConfigType' => 'PhabricatorTextConfigType',
|
||||
'PhabricatorIntExportField' => 'PhabricatorExportField',
|
||||
'PhabricatorInternalSetting' => 'PhabricatorSetting',
|
||||
'PhabricatorInternationalizationManagementExtractWorkflow' => 'PhabricatorInternationalizationManagementWorkflow',
|
||||
'PhabricatorInternationalizationManagementWorkflow' => 'PhabricatorManagementWorkflow',
|
||||
|
@ -8560,6 +8568,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorIteratorFileUploadSource' => 'PhabricatorFileUploadSource',
|
||||
'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider',
|
||||
'PhabricatorJSONConfigType' => 'PhabricatorTextConfigType',
|
||||
'PhabricatorJSONExportFormat' => 'PhabricatorExportFormat',
|
||||
'PhabricatorJavelinLinter' => 'ArcanistLinter',
|
||||
'PhabricatorJiraIssueHasObjectEdgeType' => 'PhabricatorEdgeType',
|
||||
'PhabricatorJumpNavHandler' => 'Phobject',
|
||||
|
@ -9922,6 +9931,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorTextAreaEditField' => 'PhabricatorEditField',
|
||||
'PhabricatorTextConfigType' => 'PhabricatorConfigType',
|
||||
'PhabricatorTextEditField' => 'PhabricatorEditField',
|
||||
'PhabricatorTextExportFormat' => 'PhabricatorExportFormat',
|
||||
'PhabricatorTextListConfigType' => 'PhabricatorTextConfigType',
|
||||
'PhabricatorTime' => 'Phobject',
|
||||
'PhabricatorTimeFormatSetting' => 'PhabricatorSelectSetting',
|
||||
|
|
|
@ -73,7 +73,7 @@ final class DiffusionPullLogSearchEngine
|
|||
id(new PhabricatorStringExportField())
|
||||
->setKey('result')
|
||||
->setLabel(pht('Result')),
|
||||
id(new PhabricatorStringExportField())
|
||||
id(new PhabricatorIntExportField())
|
||||
->setKey('code')
|
||||
->setLabel(pht('Code')),
|
||||
id(new PhabricatorEpochExportField())
|
||||
|
|
|
@ -413,42 +413,80 @@ final class PhabricatorApplicationSearchController
|
|||
$filename = phutil_utf8_strtolower($filename);
|
||||
$filename = PhabricatorFile::normalizeFileName($filename);
|
||||
|
||||
$formats = PhabricatorExportFormat::getAllEnabledExportFormats();
|
||||
$format_options = mpull($formats, 'getExportFormatName');
|
||||
|
||||
$errors = array();
|
||||
|
||||
$e_format = null;
|
||||
if ($request->isFormPost()) {
|
||||
$query = $engine->buildQueryFromSavedQuery($saved_query);
|
||||
$format_key = $request->getStr('format');
|
||||
$format = idx($formats, $format_key);
|
||||
|
||||
// NOTE: We aren't reading the pager from the request. Exports always
|
||||
// affect the entire result set.
|
||||
$pager = $engine->newPagerForSavedQuery($saved_query);
|
||||
$pager->setPageSize(0x7FFFFFFF);
|
||||
if (!$format) {
|
||||
$e_format = pht('Invalid');
|
||||
$errors[] = pht('Choose a valid export format.');
|
||||
}
|
||||
|
||||
$objects = $engine->executeQuery($query, $pager);
|
||||
if (!$errors) {
|
||||
$query = $engine->buildQueryFromSavedQuery($saved_query);
|
||||
|
||||
$extension = 'json';
|
||||
$mime_type = 'application/json';
|
||||
$filename = $filename.'.'.$extension;
|
||||
// NOTE: We aren't reading the pager from the request. Exports always
|
||||
// affect the entire result set.
|
||||
$pager = $engine->newPagerForSavedQuery($saved_query);
|
||||
$pager->setPageSize(0x7FFFFFFF);
|
||||
|
||||
$result = $engine->newExport($objects);
|
||||
$result = id(new PhutilJSON())
|
||||
->encodeAsList($result);
|
||||
$objects = $engine->executeQuery($query, $pager);
|
||||
|
||||
$file = PhabricatorFile::newFromFileData(
|
||||
$result,
|
||||
array(
|
||||
'name' => $filename,
|
||||
'authorPHID' => $viewer->getPHID(),
|
||||
'ttl.relative' => phutil_units('15 minutes in seconds'),
|
||||
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
|
||||
'mime-type' => $mime_type,
|
||||
));
|
||||
$extension = $format->getFileExtension();
|
||||
$mime_type = $format->getMIMEContentType();
|
||||
$filename = $filename.'.'.$extension;
|
||||
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Download Results'))
|
||||
->appendParagraph(
|
||||
pht('Click the download button to download the exported data.'))
|
||||
->addCancelButton($cancel_uri, pht('Done'))
|
||||
->setSubmitURI($file->getDownloadURI())
|
||||
->setDisableWorkflowOnSubmit(true)
|
||||
->addSubmitButton(pht('Download Results'));
|
||||
$format = clone $format;
|
||||
$format->setViewer($viewer);
|
||||
|
||||
$export_data = $engine->newExport($objects);
|
||||
|
||||
if (count($export_data) !== count($objects)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Search engine exported the wrong number of objects, expected '.
|
||||
'%s but got %s.',
|
||||
phutil_count($objects),
|
||||
phutil_count($export_data)));
|
||||
}
|
||||
|
||||
$objects = array_values($objects);
|
||||
$export_data = array_values($export_data);
|
||||
|
||||
$field_list = $engine->newExportFieldList();
|
||||
$field_list = mpull($field_list, null, 'getKey');
|
||||
|
||||
for ($ii = 0; $ii < count($objects); $ii++) {
|
||||
$format->addObject($objects[$ii], $field_list, $export_data[$ii]);
|
||||
}
|
||||
|
||||
$export_result = $format->newFileData();
|
||||
|
||||
$file = PhabricatorFile::newFromFileData(
|
||||
$export_result,
|
||||
array(
|
||||
'name' => $filename,
|
||||
'authorPHID' => $viewer->getPHID(),
|
||||
'ttl.relative' => phutil_units('15 minutes in seconds'),
|
||||
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
|
||||
'mime-type' => $mime_type,
|
||||
));
|
||||
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Download Results'))
|
||||
->appendParagraph(
|
||||
pht('Click the download button to download the exported data.'))
|
||||
->addCancelButton($cancel_uri, pht('Done'))
|
||||
->setSubmitURI($file->getDownloadURI())
|
||||
->setDisableWorkflowOnSubmit(true)
|
||||
->addSubmitButton(pht('Download Data'));
|
||||
}
|
||||
}
|
||||
|
||||
$export_form = id(new AphrontFormView())
|
||||
|
@ -457,13 +495,12 @@ final class PhabricatorApplicationSearchController
|
|||
id(new AphrontFormSelectControl())
|
||||
->setName('format')
|
||||
->setLabel(pht('Format'))
|
||||
->setOptions(
|
||||
array(
|
||||
'json' => 'JSON',
|
||||
)));
|
||||
->setError($e_format)
|
||||
->setOptions($format_options));
|
||||
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Export Results'))
|
||||
->setErrors($errors)
|
||||
->appendForm($export_form)
|
||||
->addCancelButton($cancel_uri)
|
||||
->addSubmitButton(pht('Continue'));
|
||||
|
@ -826,7 +863,7 @@ final class PhabricatorApplicationSearchController
|
|||
$export_uri = $engine->getExportURI($query_key);
|
||||
$actions[] = id(new PhabricatorActionView())
|
||||
->setIcon('fa-download')
|
||||
->setName(pht('Export Results'))
|
||||
->setName(pht('Export Data'))
|
||||
->setWorkflow(true)
|
||||
->setHref($export_uri);
|
||||
}
|
||||
|
|
|
@ -1454,6 +1454,10 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject {
|
|||
return (bool)$fields;
|
||||
}
|
||||
|
||||
final public function newExportFieldList() {
|
||||
return $this->newExportFields();
|
||||
}
|
||||
|
||||
protected function newExportFields() {
|
||||
return array();
|
||||
}
|
||||
|
|
47
src/infrastructure/export/PhabricatorCSVExportFormat.php
Normal file
47
src/infrastructure/export/PhabricatorCSVExportFormat.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorCSVExportFormat
|
||||
extends PhabricatorExportFormat {
|
||||
|
||||
const EXPORTKEY = 'csv';
|
||||
|
||||
private $rows = array();
|
||||
|
||||
public function getExportFormatName() {
|
||||
return pht('Comma-Separated Values (.csv)');
|
||||
}
|
||||
|
||||
public function isExportFormatEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getFileExtension() {
|
||||
return 'csv';
|
||||
}
|
||||
|
||||
public function getMIMEContentType() {
|
||||
return 'text/csv';
|
||||
}
|
||||
|
||||
public function addObject($object, array $fields, array $map) {
|
||||
$values = array();
|
||||
foreach ($fields as $key => $field) {
|
||||
$value = $map[$key];
|
||||
$value = $field->getTextValue($value);
|
||||
|
||||
if (preg_match('/\s|,|\"/', $value)) {
|
||||
$value = str_replace('"', '""', $value);
|
||||
$value = '"'.$value.'"';
|
||||
}
|
||||
|
||||
$values[] = $value;
|
||||
}
|
||||
|
||||
$this->rows[] = implode(',', $values);
|
||||
}
|
||||
|
||||
public function newFileData() {
|
||||
return implode("\n", $this->rows);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,27 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorEpochExportField
|
||||
extends PhabricatorExportField {}
|
||||
extends PhabricatorExportField {
|
||||
|
||||
private $zone;
|
||||
|
||||
public function getTextValue($value) {
|
||||
if (!isset($this->zone)) {
|
||||
$this->zone = new DateTimeZone('UTC');
|
||||
}
|
||||
|
||||
try {
|
||||
$date = new DateTime('@'.$value);
|
||||
} catch (Exception $ex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$date->setTimezone($this->zone);
|
||||
return $date->format('c');
|
||||
}
|
||||
|
||||
public function getNaturalValue($value) {
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -24,4 +24,12 @@ abstract class PhabricatorExportField
|
|||
return $this->label;
|
||||
}
|
||||
|
||||
public function getTextValue($value) {
|
||||
return (string)$this->getNaturalValue($value);
|
||||
}
|
||||
|
||||
public function getNaturalValue($value) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
51
src/infrastructure/export/PhabricatorExportFormat.php
Normal file
51
src/infrastructure/export/PhabricatorExportFormat.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorExportFormat
|
||||
extends Phobject {
|
||||
|
||||
private $viewer;
|
||||
|
||||
final public function getExportFormatKey() {
|
||||
return $this->getPhobjectClassConstant('EXPORTKEY');
|
||||
}
|
||||
|
||||
final public function setViewer(PhabricatorUser $viewer) {
|
||||
$this->viewer = $viewer;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getViewer() {
|
||||
return $this->viewer;
|
||||
}
|
||||
|
||||
abstract public function getExportFormatName();
|
||||
abstract public function getMIMEContentType();
|
||||
abstract public function getFileExtension();
|
||||
|
||||
abstract public function addObject($object, array $fields, array $map);
|
||||
abstract public function newFileData();
|
||||
|
||||
public function isExportFormatEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
final public static function getAllExportFormats() {
|
||||
return id(new PhutilClassMapQuery())
|
||||
->setAncestorClass(__CLASS__)
|
||||
->setUniqueMethod('getExportFormatKey')
|
||||
->execute();
|
||||
}
|
||||
|
||||
final public static function getAllEnabledExportFormats() {
|
||||
$formats = self::getAllExportFormats();
|
||||
|
||||
foreach ($formats as $key => $format) {
|
||||
if (!$format->isExportFormatEnabled()) {
|
||||
unset($formats[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $formats;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,10 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorIDExportField
|
||||
extends PhabricatorExportField {}
|
||||
extends PhabricatorExportField {
|
||||
|
||||
public function getNaturalValue($value) {
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
10
src/infrastructure/export/PhabricatorIntExportField.php
Normal file
10
src/infrastructure/export/PhabricatorIntExportField.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorIntExportField
|
||||
extends PhabricatorExportField {
|
||||
|
||||
public function getNaturalValue($value) {
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
}
|
43
src/infrastructure/export/PhabricatorJSONExportFormat.php
Normal file
43
src/infrastructure/export/PhabricatorJSONExportFormat.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorJSONExportFormat
|
||||
extends PhabricatorExportFormat {
|
||||
|
||||
const EXPORTKEY = 'json';
|
||||
|
||||
private $objects = array();
|
||||
|
||||
public function getExportFormatName() {
|
||||
return 'JSON (.json)';
|
||||
}
|
||||
|
||||
public function isExportFormatEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getFileExtension() {
|
||||
return 'json';
|
||||
}
|
||||
|
||||
public function getMIMEContentType() {
|
||||
return 'application/json';
|
||||
}
|
||||
|
||||
public function addObject($object, array $fields, array $map) {
|
||||
$values = array();
|
||||
foreach ($fields as $key => $field) {
|
||||
$value = $map[$key];
|
||||
$value = $field->getNaturalValue($value);
|
||||
|
||||
$values[$key] = $value;
|
||||
}
|
||||
|
||||
$this->objects[] = $values;
|
||||
}
|
||||
|
||||
public function newFileData() {
|
||||
return id(new PhutilJSON())
|
||||
->encodeAsList($this->objects);
|
||||
}
|
||||
|
||||
}
|
43
src/infrastructure/export/PhabricatorTextExportFormat.php
Normal file
43
src/infrastructure/export/PhabricatorTextExportFormat.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorTextExportFormat
|
||||
extends PhabricatorExportFormat {
|
||||
|
||||
const EXPORTKEY = 'text';
|
||||
|
||||
private $rows = array();
|
||||
|
||||
public function getExportFormatName() {
|
||||
return 'Tab-Separated Text (.txt)';
|
||||
}
|
||||
|
||||
public function isExportFormatEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getFileExtension() {
|
||||
return 'txt';
|
||||
}
|
||||
|
||||
public function getMIMEContentType() {
|
||||
return 'text/plain';
|
||||
}
|
||||
|
||||
public function addObject($object, array $fields, array $map) {
|
||||
$values = array();
|
||||
foreach ($fields as $key => $field) {
|
||||
$value = $map[$key];
|
||||
$value = $field->getTextValue($value);
|
||||
$value = addcslashes($value, "\0..\37\\\177..\377");
|
||||
|
||||
$values[] = $value;
|
||||
}
|
||||
|
||||
$this->rows[] = implode("\t", $values);
|
||||
}
|
||||
|
||||
public function newFileData() {
|
||||
return implode("\n", $this->rows)."\n";
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue