1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-03-28 12:08: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:
epriestley 2018-01-25 17:15:49 -08:00
parent c0b8e4784b
commit a79bb55f3f
12 changed files with 319 additions and 37 deletions

View file

@ -2231,6 +2231,7 @@ phutil_register_library_map(array(
'PhabricatorBulkEngine' => 'applications/transactions/bulk/PhabricatorBulkEngine.php', 'PhabricatorBulkEngine' => 'applications/transactions/bulk/PhabricatorBulkEngine.php',
'PhabricatorBulkManagementMakeSilentWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementMakeSilentWorkflow.php', 'PhabricatorBulkManagementMakeSilentWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementMakeSilentWorkflow.php',
'PhabricatorBulkManagementWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementWorkflow.php', 'PhabricatorBulkManagementWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementWorkflow.php',
'PhabricatorCSVExportFormat' => 'infrastructure/export/PhabricatorCSVExportFormat.php',
'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php', 'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php',
'PhabricatorCacheEngine' => 'applications/system/engine/PhabricatorCacheEngine.php', 'PhabricatorCacheEngine' => 'applications/system/engine/PhabricatorCacheEngine.php',
'PhabricatorCacheEngineExtension' => 'applications/system/engine/PhabricatorCacheEngineExtension.php', 'PhabricatorCacheEngineExtension' => 'applications/system/engine/PhabricatorCacheEngineExtension.php',
@ -2844,6 +2845,7 @@ phutil_register_library_map(array(
'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php', 'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php',
'PhabricatorExecFutureFileUploadSource' => 'applications/files/uploadsource/PhabricatorExecFutureFileUploadSource.php', 'PhabricatorExecFutureFileUploadSource' => 'applications/files/uploadsource/PhabricatorExecFutureFileUploadSource.php',
'PhabricatorExportField' => 'infrastructure/export/PhabricatorExportField.php', 'PhabricatorExportField' => 'infrastructure/export/PhabricatorExportField.php',
'PhabricatorExportFormat' => 'infrastructure/export/PhabricatorExportFormat.php',
'PhabricatorExtendedPolicyInterface' => 'applications/policy/interface/PhabricatorExtendedPolicyInterface.php', 'PhabricatorExtendedPolicyInterface' => 'applications/policy/interface/PhabricatorExtendedPolicyInterface.php',
'PhabricatorExtendingPhabricatorConfigOptions' => 'applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php', 'PhabricatorExtendingPhabricatorConfigOptions' => 'applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php',
'PhabricatorExtensionsSetupCheck' => 'applications/config/check/PhabricatorExtensionsSetupCheck.php', 'PhabricatorExtensionsSetupCheck' => 'applications/config/check/PhabricatorExtensionsSetupCheck.php',
@ -3087,6 +3089,7 @@ phutil_register_library_map(array(
'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php', 'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php',
'PhabricatorInstructionsEditField' => 'applications/transactions/editfield/PhabricatorInstructionsEditField.php', 'PhabricatorInstructionsEditField' => 'applications/transactions/editfield/PhabricatorInstructionsEditField.php',
'PhabricatorIntConfigType' => 'applications/config/type/PhabricatorIntConfigType.php', 'PhabricatorIntConfigType' => 'applications/config/type/PhabricatorIntConfigType.php',
'PhabricatorIntExportField' => 'infrastructure/export/PhabricatorIntExportField.php',
'PhabricatorInternalSetting' => 'applications/settings/setting/PhabricatorInternalSetting.php', 'PhabricatorInternalSetting' => 'applications/settings/setting/PhabricatorInternalSetting.php',
'PhabricatorInternationalizationManagementExtractWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php', 'PhabricatorInternationalizationManagementExtractWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php',
'PhabricatorInternationalizationManagementWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementWorkflow.php', 'PhabricatorInternationalizationManagementWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementWorkflow.php',
@ -3096,6 +3099,7 @@ phutil_register_library_map(array(
'PhabricatorIteratorFileUploadSource' => 'applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php', 'PhabricatorIteratorFileUploadSource' => 'applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php',
'PhabricatorJIRAAuthProvider' => 'applications/auth/provider/PhabricatorJIRAAuthProvider.php', 'PhabricatorJIRAAuthProvider' => 'applications/auth/provider/PhabricatorJIRAAuthProvider.php',
'PhabricatorJSONConfigType' => 'applications/config/type/PhabricatorJSONConfigType.php', 'PhabricatorJSONConfigType' => 'applications/config/type/PhabricatorJSONConfigType.php',
'PhabricatorJSONExportFormat' => 'infrastructure/export/PhabricatorJSONExportFormat.php',
'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php', 'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php',
'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php', 'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php',
'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.php', 'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.php',
@ -4245,6 +4249,7 @@ phutil_register_library_map(array(
'PhabricatorTextAreaEditField' => 'applications/transactions/editfield/PhabricatorTextAreaEditField.php', 'PhabricatorTextAreaEditField' => 'applications/transactions/editfield/PhabricatorTextAreaEditField.php',
'PhabricatorTextConfigType' => 'applications/config/type/PhabricatorTextConfigType.php', 'PhabricatorTextConfigType' => 'applications/config/type/PhabricatorTextConfigType.php',
'PhabricatorTextEditField' => 'applications/transactions/editfield/PhabricatorTextEditField.php', 'PhabricatorTextEditField' => 'applications/transactions/editfield/PhabricatorTextEditField.php',
'PhabricatorTextExportFormat' => 'infrastructure/export/PhabricatorTextExportFormat.php',
'PhabricatorTextListConfigType' => 'applications/config/type/PhabricatorTextListConfigType.php', 'PhabricatorTextListConfigType' => 'applications/config/type/PhabricatorTextListConfigType.php',
'PhabricatorTime' => 'infrastructure/time/PhabricatorTime.php', 'PhabricatorTime' => 'infrastructure/time/PhabricatorTime.php',
'PhabricatorTimeFormatSetting' => 'applications/settings/setting/PhabricatorTimeFormatSetting.php', 'PhabricatorTimeFormatSetting' => 'applications/settings/setting/PhabricatorTimeFormatSetting.php',
@ -7564,6 +7569,7 @@ phutil_register_library_map(array(
'PhabricatorBulkEngine' => 'Phobject', 'PhabricatorBulkEngine' => 'Phobject',
'PhabricatorBulkManagementMakeSilentWorkflow' => 'PhabricatorBulkManagementWorkflow', 'PhabricatorBulkManagementMakeSilentWorkflow' => 'PhabricatorBulkManagementWorkflow',
'PhabricatorBulkManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorBulkManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorCSVExportFormat' => 'PhabricatorExportFormat',
'PhabricatorCacheDAO' => 'PhabricatorLiskDAO', 'PhabricatorCacheDAO' => 'PhabricatorLiskDAO',
'PhabricatorCacheEngine' => 'Phobject', 'PhabricatorCacheEngine' => 'Phobject',
'PhabricatorCacheEngineExtension' => 'Phobject', 'PhabricatorCacheEngineExtension' => 'Phobject',
@ -8268,6 +8274,7 @@ phutil_register_library_map(array(
'PhabricatorExampleEventListener' => 'PhabricatorEventListener', 'PhabricatorExampleEventListener' => 'PhabricatorEventListener',
'PhabricatorExecFutureFileUploadSource' => 'PhabricatorFileUploadSource', 'PhabricatorExecFutureFileUploadSource' => 'PhabricatorFileUploadSource',
'PhabricatorExportField' => 'Phobject', 'PhabricatorExportField' => 'Phobject',
'PhabricatorExportFormat' => 'Phobject',
'PhabricatorExtendingPhabricatorConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorExtendingPhabricatorConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorExtensionsSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorExtensionsSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorExternalAccount' => array( 'PhabricatorExternalAccount' => array(
@ -8551,6 +8558,7 @@ phutil_register_library_map(array(
'PhabricatorInlineSummaryView' => 'AphrontView', 'PhabricatorInlineSummaryView' => 'AphrontView',
'PhabricatorInstructionsEditField' => 'PhabricatorEditField', 'PhabricatorInstructionsEditField' => 'PhabricatorEditField',
'PhabricatorIntConfigType' => 'PhabricatorTextConfigType', 'PhabricatorIntConfigType' => 'PhabricatorTextConfigType',
'PhabricatorIntExportField' => 'PhabricatorExportField',
'PhabricatorInternalSetting' => 'PhabricatorSetting', 'PhabricatorInternalSetting' => 'PhabricatorSetting',
'PhabricatorInternationalizationManagementExtractWorkflow' => 'PhabricatorInternationalizationManagementWorkflow', 'PhabricatorInternationalizationManagementExtractWorkflow' => 'PhabricatorInternationalizationManagementWorkflow',
'PhabricatorInternationalizationManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorInternationalizationManagementWorkflow' => 'PhabricatorManagementWorkflow',
@ -8560,6 +8568,7 @@ phutil_register_library_map(array(
'PhabricatorIteratorFileUploadSource' => 'PhabricatorFileUploadSource', 'PhabricatorIteratorFileUploadSource' => 'PhabricatorFileUploadSource',
'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider', 'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider',
'PhabricatorJSONConfigType' => 'PhabricatorTextConfigType', 'PhabricatorJSONConfigType' => 'PhabricatorTextConfigType',
'PhabricatorJSONExportFormat' => 'PhabricatorExportFormat',
'PhabricatorJavelinLinter' => 'ArcanistLinter', 'PhabricatorJavelinLinter' => 'ArcanistLinter',
'PhabricatorJiraIssueHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorJiraIssueHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorJumpNavHandler' => 'Phobject', 'PhabricatorJumpNavHandler' => 'Phobject',
@ -9922,6 +9931,7 @@ phutil_register_library_map(array(
'PhabricatorTextAreaEditField' => 'PhabricatorEditField', 'PhabricatorTextAreaEditField' => 'PhabricatorEditField',
'PhabricatorTextConfigType' => 'PhabricatorConfigType', 'PhabricatorTextConfigType' => 'PhabricatorConfigType',
'PhabricatorTextEditField' => 'PhabricatorEditField', 'PhabricatorTextEditField' => 'PhabricatorEditField',
'PhabricatorTextExportFormat' => 'PhabricatorExportFormat',
'PhabricatorTextListConfigType' => 'PhabricatorTextConfigType', 'PhabricatorTextListConfigType' => 'PhabricatorTextConfigType',
'PhabricatorTime' => 'Phobject', 'PhabricatorTime' => 'Phobject',
'PhabricatorTimeFormatSetting' => 'PhabricatorSelectSetting', 'PhabricatorTimeFormatSetting' => 'PhabricatorSelectSetting',

View file

@ -73,7 +73,7 @@ final class DiffusionPullLogSearchEngine
id(new PhabricatorStringExportField()) id(new PhabricatorStringExportField())
->setKey('result') ->setKey('result')
->setLabel(pht('Result')), ->setLabel(pht('Result')),
id(new PhabricatorStringExportField()) id(new PhabricatorIntExportField())
->setKey('code') ->setKey('code')
->setLabel(pht('Code')), ->setLabel(pht('Code')),
id(new PhabricatorEpochExportField()) id(new PhabricatorEpochExportField())

View file

@ -413,42 +413,80 @@ final class PhabricatorApplicationSearchController
$filename = phutil_utf8_strtolower($filename); $filename = phutil_utf8_strtolower($filename);
$filename = PhabricatorFile::normalizeFileName($filename); $filename = PhabricatorFile::normalizeFileName($filename);
$formats = PhabricatorExportFormat::getAllEnabledExportFormats();
$format_options = mpull($formats, 'getExportFormatName');
$errors = array();
$e_format = null;
if ($request->isFormPost()) { 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 if (!$format) {
// affect the entire result set. $e_format = pht('Invalid');
$pager = $engine->newPagerForSavedQuery($saved_query); $errors[] = pht('Choose a valid export format.');
$pager->setPageSize(0x7FFFFFFF); }
$objects = $engine->executeQuery($query, $pager); if (!$errors) {
$query = $engine->buildQueryFromSavedQuery($saved_query);
$extension = 'json'; // NOTE: We aren't reading the pager from the request. Exports always
$mime_type = 'application/json'; // affect the entire result set.
$filename = $filename.'.'.$extension; $pager = $engine->newPagerForSavedQuery($saved_query);
$pager->setPageSize(0x7FFFFFFF);
$result = $engine->newExport($objects); $objects = $engine->executeQuery($query, $pager);
$result = id(new PhutilJSON())
->encodeAsList($result);
$file = PhabricatorFile::newFromFileData( $extension = $format->getFileExtension();
$result, $mime_type = $format->getMIMEContentType();
array( $filename = $filename.'.'.$extension;
'name' => $filename,
'authorPHID' => $viewer->getPHID(),
'ttl.relative' => phutil_units('15 minutes in seconds'),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
'mime-type' => $mime_type,
));
return $this->newDialog() $format = clone $format;
->setTitle(pht('Download Results')) $format->setViewer($viewer);
->appendParagraph(
pht('Click the download button to download the exported data.')) $export_data = $engine->newExport($objects);
->addCancelButton($cancel_uri, pht('Done'))
->setSubmitURI($file->getDownloadURI()) if (count($export_data) !== count($objects)) {
->setDisableWorkflowOnSubmit(true) throw new Exception(
->addSubmitButton(pht('Download Results')); 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()) $export_form = id(new AphrontFormView())
@ -457,13 +495,12 @@ final class PhabricatorApplicationSearchController
id(new AphrontFormSelectControl()) id(new AphrontFormSelectControl())
->setName('format') ->setName('format')
->setLabel(pht('Format')) ->setLabel(pht('Format'))
->setOptions( ->setError($e_format)
array( ->setOptions($format_options));
'json' => 'JSON',
)));
return $this->newDialog() return $this->newDialog()
->setTitle(pht('Export Results')) ->setTitle(pht('Export Results'))
->setErrors($errors)
->appendForm($export_form) ->appendForm($export_form)
->addCancelButton($cancel_uri) ->addCancelButton($cancel_uri)
->addSubmitButton(pht('Continue')); ->addSubmitButton(pht('Continue'));
@ -826,7 +863,7 @@ final class PhabricatorApplicationSearchController
$export_uri = $engine->getExportURI($query_key); $export_uri = $engine->getExportURI($query_key);
$actions[] = id(new PhabricatorActionView()) $actions[] = id(new PhabricatorActionView())
->setIcon('fa-download') ->setIcon('fa-download')
->setName(pht('Export Results')) ->setName(pht('Export Data'))
->setWorkflow(true) ->setWorkflow(true)
->setHref($export_uri); ->setHref($export_uri);
} }

View file

@ -1454,6 +1454,10 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject {
return (bool)$fields; return (bool)$fields;
} }
final public function newExportFieldList() {
return $this->newExportFields();
}
protected function newExportFields() { protected function newExportFields() {
return array(); return array();
} }

View 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);
}
}

View file

@ -1,4 +1,27 @@
<?php <?php
final class PhabricatorEpochExportField 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;
}
}

View file

@ -24,4 +24,12 @@ abstract class PhabricatorExportField
return $this->label; return $this->label;
} }
public function getTextValue($value) {
return (string)$this->getNaturalValue($value);
}
public function getNaturalValue($value) {
return $value;
}
} }

View 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;
}
}

View file

@ -1,4 +1,10 @@
<?php <?php
final class PhabricatorIDExportField final class PhabricatorIDExportField
extends PhabricatorExportField {} extends PhabricatorExportField {
public function getNaturalValue($value) {
return (int)$value;
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhabricatorIntExportField
extends PhabricatorExportField {
public function getNaturalValue($value) {
return (int)$value;
}
}

View 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);
}
}

View 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";
}
}