diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index aebfab9a26..39dae22d77 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -410,6 +410,7 @@ phutil_register_library_map(array( 'ManiphestController' => 'applications/maniphest/controller/base', 'ManiphestDAO' => 'applications/maniphest/storage/base', 'ManiphestDefaultTaskExtensions' => 'applications/maniphest/extensions/task', + 'ManiphestExportController' => 'applications/maniphest/controller/export', 'ManiphestReplyHandler' => 'applications/maniphest/replyhandler', 'ManiphestReportController' => 'applications/maniphest/controller/report', 'ManiphestTask' => 'applications/maniphest/storage/task', @@ -1210,6 +1211,7 @@ phutil_register_library_map(array( 'ManiphestController' => 'PhabricatorController', 'ManiphestDAO' => 'PhabricatorLiskDAO', 'ManiphestDefaultTaskExtensions' => 'ManiphestTaskExtensions', + 'ManiphestExportController' => 'ManiphestController', 'ManiphestReplyHandler' => 'PhabricatorMailReplyHandler', 'ManiphestReportController' => 'ManiphestController', 'ManiphestTask' => 'ManiphestDAO', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 14fa0b7aca..c5da89eadf 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -207,6 +207,7 @@ class AphrontDefaultApplicationConfiguration 'save/' => 'ManiphestTransactionSaveController', 'preview/(?P\d+)/$' => 'ManiphestTransactionPreviewController', ), + 'export/(?P[^/]+)/$' => 'ManiphestExportController', ), '/T(?P\d+)$' => 'ManiphestTaskDetailController', diff --git a/src/applications/maniphest/controller/export/ManiphestExportController.php b/src/applications/maniphest/controller/export/ManiphestExportController.php new file mode 100644 index 0000000000..c0ffc4a8ca --- /dev/null +++ b/src/applications/maniphest/controller/export/ManiphestExportController.php @@ -0,0 +1,201 @@ +key = $data['key']; + return $this; + } + + /** + * @phutil-external-symbol class Spreadsheet_Excel_Writer + */ + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $ok = @include_once 'Spreadsheet/Excel/Writer.php'; + if (!$ok) { + $dialog = new AphrontDialogView(); + $dialog->setUser($user); + + $dialog->setTitle('Excel Export Not Configured'); + $dialog->appendChild( + '

This system does not have Spreadsheet_Excel_Writer installed. '. + 'This software component is required to export tasks to Excel. Have '. + 'your system administrator install it with:

'. + '
'. + '

$ sudo pear install Spreadsheet_Excel_Writer

'); + + $dialog->addCancelButton('/maniphest/'); + return id(new AphrontDialogResponse())->setDialog($dialog); + } + + $query = id(new PhabricatorSearchQuery())->loadOneWhere( + 'queryKey = %s', + $this->key); + if (!$query) { + return new Aphront404Response(); + } + + if (!$request->isDialogFormPost()) { + $dialog = new AphrontDialogView(); + $dialog->setUser($user); + + $dialog->setTitle('Export Tasks to Excel'); + $dialog->appendChild( + '

Do you want to export the query results to Excel?

'); + + $dialog->addCancelButton('/maniphest/'); + $dialog->addSubmitButton('Export to Excel'); + return id(new AphrontDialogResponse())->setDialog($dialog); + + } + + $query->setParameter('limit', null); + $query->setParameter('offset', null); + $query->setParameter('order', 'p'); + $query->setParameter('group', 'n'); + + list($tasks, $handles) = ManiphestTaskListController::loadTasks($query); + // Ungroup tasks. + $tasks = array_mergev($tasks); + + $all_projects = array_mergev(mpull($tasks, 'getProjectPHIDs')); + $project_handles = id(new PhabricatorObjectHandleData($all_projects)) + ->loadHandles(); + $handles += $project_handles; + + $workbook = new Spreadsheet_Excel_Writer(); + $sheet = $workbook->addWorksheet('Exported Maniphest Tasks'); + + $date_format = $workbook->addFormat(); + $date_format->setNumFormat('M/D/YYYY h:mm AM/PM'); + + $widths = array( + null, + 20, + null, + 15, + 20, + 20, + 75, + 40, + 30, + 400, + ); + + foreach ($widths as $col => $width) { + if ($width !== null) { + $sheet->setColumn($col, $col, $width); + } + } + + $status_map = ManiphestTaskStatus::getTaskStatusMap(); + $pri_map = ManiphestTaskPriority::getTaskPriorityMap(); + + $rows = array(); + $rows[] = array( + 'ID', + 'Owner', + 'Status', + 'Priority', + 'Date Created', + 'Date Updated', + 'Title', + 'Projects', + 'URI', + 'Description', + ); + $formats = array( + null, + null, + null, + null, + $date_format, + $date_format, + null, + null, + null, + null, + ); + + $header_format = $workbook->addFormat(); + $header_format->setBold(); + + foreach ($tasks as $task) { + $task_owner = null; + if ($task->getOwnerPHID()) { + $task_owner = $handles[$task->getOwnerPHID()]->getName(); + } + + $projects = array(); + foreach ($task->getProjectPHIDs() as $phid) { + $projects[] = $handles[$phid]->getName(); + } + $projects = implode(', ', $projects); + + $rows[] = array( + 'T'.$task->getID(), + $task_owner, + idx($status_map, $task->getStatus(), '?'), + idx($pri_map, $task->getPriority(), '?'), + $this->computeExcelDate($task->getDateCreated()), + $this->computeExcelDate($task->getDateModified()), + $task->getTitle(), + $projects, + PhabricatorEnv::getProductionURI('/T'.$task->getID()), + $task->getDescription(), + ); + } + + foreach ($rows as $row => $cols) { + foreach ($cols as $col => $spec) { + if ($row == 0) { + $fmt = $header_format; + } else { + $fmt = $formats[$col]; + } + $sheet->write($row, $col, $spec, $fmt); + } + } + + ob_start(); + $workbook->close(); + $data = ob_get_clean(); + + return id(new AphrontFileResponse()) + ->setMimeType('application/vnd.ms-excel') + ->setDownload('maniphest_tasks_'.date('Ymd').'.xls') + ->setContent($data); + } + + private function computeExcelDate($epoch) { + $seconds_per_day = (60 * 60 * 24); + $offset = ($seconds_per_day * 25569); + + return ($epoch + $offset) / $seconds_per_day; + } + +} diff --git a/src/applications/maniphest/controller/export/__init__.php b/src/applications/maniphest/controller/export/__init__.php new file mode 100644 index 0000000000..b822b6cbcb --- /dev/null +++ b/src/applications/maniphest/controller/export/__init__.php @@ -0,0 +1,24 @@ +getInt('page'); $page_size = self::DEFAULT_PAGE_SIZE; - list($tasks, $handles, $total_count) = $this->loadTasks( - $user_phids, - $project_phids, - $task_ids, + $query = new PhabricatorSearchQuery(); + $query->setQuery('<>'); + $query->setParameters( array( - 'status' => $status_map, - 'group' => $grouping, - 'order' => $order, - 'offset' => $page, - 'limit' => $page_size, + 'view' => $this->view, + 'userPHIDs' => $user_phids, + 'projectPHIDs' => $project_phids, + 'taskIDs' => $task_ids, + 'group' => $grouping, + 'order' => $order, + 'offset' => $page, + 'limit' => $page_size, + 'status' => $status_map, )); + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $query->save(); + unset($unguarded); + + list($tasks, $handles, $total_count) = self::loadTasks($query); + $form = id(new AphrontFormView()) ->setUser($user) ->setAction($request->getRequestURI()); @@ -231,7 +240,7 @@ class ManiphestTaskListController extends ManiphestController { } - $selector->appendChild($this->renderBatchEditor()); + $selector->appendChild($this->renderBatchEditor($query)); $selector = phabricator_render_form( $user, @@ -252,17 +261,17 @@ class ManiphestTaskListController extends ManiphestController { )); } - private function loadTasks( - array $user_phids, - array $project_phids, - array $task_ids, - array $dict) { + public static function loadTasks(PhabricatorSearchQuery $search_query) { + + $user_phids = $search_query->getParameter('userPHIDs', array()); + $project_phids = $search_query->getParameter('projectPHIDs', array()); + $task_ids = $search_query->getParameter('taskIDs', array()); $query = new ManiphestTaskQuery(); $query->withProjects($project_phids); $query->withTaskIDs($task_ids); - $status = $dict['status']; + $status = $search_query->getParameter('status', 'all'); if (!empty($status['open']) && !empty($status['closed'])) { $query->withStatus(ManiphestTaskQuery::STATUS_ANY); } else if (!empty($status['open'])) { @@ -271,7 +280,7 @@ class ManiphestTaskListController extends ManiphestController { $query->withStatus(ManiphestTaskQuery::STATUS_CLOSED); } - switch ($this->view) { + switch ($search_query->getParameter('view')) { case 'action': $query->withOwners($user_phids); break; @@ -299,7 +308,7 @@ class ManiphestTaskListController extends ManiphestController { $query->setOrderBy( idx( $order_map, - $dict['order'], + $search_query->getParameter('order'), ManiphestTaskQuery::ORDER_MODIFIED)); $group_map = array( @@ -310,12 +319,12 @@ class ManiphestTaskListController extends ManiphestController { $query->setGroupBy( idx( $group_map, - $dict['group'], + $search_query->getParameter('group'), ManiphestTaskQuery::GROUP_NONE)); $query->setCalculateRows(true); - $query->setLimit($dict['limit']); - $query->setOffset($dict['offset']); + $query->setLimit($search_query->getParameter('limit')); + $query->setOffset($search_query->getParameter('offset')); $data = $query->execute(); $total_row_count = $query->getRowCount(); @@ -325,7 +334,7 @@ class ManiphestTaskListController extends ManiphestController { $handles = id(new PhabricatorObjectHandleData($handle_phids)) ->loadHandles(); - switch ($dict['group']) { + switch ($search_query->getParameter('group')) { case 'priority': $data = mgroup($data, 'getPriority'); krsort($data); @@ -471,7 +480,7 @@ class ManiphestTaskListController extends ManiphestController { return array($group_by, $group_control); } - private function renderBatchEditor() { + private function renderBatchEditor(PhabricatorSearchQuery $search_query) { Javelin::initBehavior( 'maniphest-batch-selector', array( @@ -510,6 +519,14 @@ class ManiphestTaskListController extends ManiphestController { ), 'Batch Edit Selected Tasks »'); + $export = javelin_render_tag( + 'a', + array( + 'href' => '/maniphest/export/'.$search_query->getQueryKey().'/', + 'class' => 'grey button', + ), + 'Export Tasks to Excel...'); + return '
'. '
Batch Task Editor
'. @@ -519,6 +536,9 @@ class ManiphestTaskListController extends ManiphestController { $select_all. $select_none. ''. + ''. + $export. + ''. ''. '0 Selected Tasks'. ''. diff --git a/src/applications/maniphest/controller/tasklist/__init__.php b/src/applications/maniphest/controller/tasklist/__init__.php index c7fcc3eec3..dd8c5a941c 100644 --- a/src/applications/maniphest/controller/tasklist/__init__.php +++ b/src/applications/maniphest/controller/tasklist/__init__.php @@ -7,12 +7,14 @@ phutil_require_module('phabricator', 'aphront/response/redirect'); +phutil_require_module('phabricator', 'aphront/writeguard'); phutil_require_module('phabricator', 'applications/maniphest/constants/priority'); phutil_require_module('phabricator', 'applications/maniphest/constants/status'); phutil_require_module('phabricator', 'applications/maniphest/controller/base'); phutil_require_module('phabricator', 'applications/maniphest/query'); phutil_require_module('phabricator', 'applications/maniphest/view/tasklist'); phutil_require_module('phabricator', 'applications/phid/handle/data'); +phutil_require_module('phabricator', 'applications/search/storage/query'); phutil_require_module('phabricator', 'infrastructure/celerity/api'); phutil_require_module('phabricator', 'infrastructure/javelin/api'); phutil_require_module('phabricator', 'infrastructure/javelin/markup');