Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/maniphest/controller/ManiphestTaskListController.php b/src/applications/maniphest/controller/ManiphestTaskListController.php
index 544570afaa..9cdbd96197 100644
--- a/src/applications/maniphest/controller/ManiphestTaskListController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskListController.php
@@ -1,981 +1,976 @@
<?php
/**
* @group maniphest
*/
final class ManiphestTaskListController extends ManiphestController {
const DEFAULT_PAGE_SIZE = 1000;
private $view;
public function willProcessRequest(array $data) {
$this->view = idx($data, 'view');
}
private function getArrToStrList($key) {
$arr = $this->getRequest()->getArr($key);
$arr = implode(',', $arr);
return nonempty($arr, null);
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($request->isFormPost()) {
// Redirect to GET so URIs can be copy/pasted.
$task_ids = $request->getStr('set_tasks');
$task_ids = nonempty($task_ids, null);
$search_text = $request->getStr('set_search');
$min_priority = $request->getInt('set_lpriority');
$max_priority = $request->getInt('set_hpriority');
$uri = $request->getRequestURI()
->alter('users', $this->getArrToStrList('set_users'))
->alter('projects', $this->getArrToStrList('set_projects'))
->alter('aprojects', $this->getArrToStrList('set_aprojects'))
->alter('useraprojects', $this->getArrToStrList('set_useraprojects'))
->alter('xprojects', $this->getArrToStrList('set_xprojects'))
->alter('owners', $this->getArrToStrList('set_owners'))
->alter('authors', $this->getArrToStrList('set_authors'))
->alter('lpriority', $min_priority)
->alter('hpriority', $max_priority)
->alter('tasks', $task_ids)
->alter('search', $search_text);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$nav = $this->buildBaseSideNav();
$has_filter = array(
'action' => true,
'created' => true,
'subscribed' => true,
'triage' => true,
'projecttriage' => true,
'projectall' => true,
);
$query = null;
$key = $request->getStr('key');
if (!$key && !$this->view) {
if ($this->getDefaultQuery()) {
$key = $this->getDefaultQuery()->getQueryKey();
}
}
if ($key) {
$query = id(new PhabricatorSearchQuery())->loadOneWhere(
'queryKey = %s',
$key);
}
// If the user is running a saved query, load query parameters from that
// query. Otherwise, build a new query object from the HTTP request.
if ($query) {
$nav->selectFilter('Q:'.$query->getQueryKey(), 'custom');
$this->view = 'custom';
} else {
$this->view = $nav->selectFilter($this->view, 'action');
$query = $this->buildQueryFromRequest();
}
// Execute the query.
list($tasks, $handles, $total_count) = self::loadTasks(
$query,
$user);
// Extract information we need to render the filters from the query.
$search_text = $query->getParameter('fullTextSearch');
$user_phids = $query->getParameter('userPHIDs', array());
$task_ids = $query->getParameter('taskIDs', array());
$owner_phids = $query->getParameter('ownerPHIDs', array());
$author_phids = $query->getParameter('authorPHIDs', array());
$project_phids = $query->getParameter('projectPHIDs', array());
$any_project_phids = $query->getParameter(
'anyProjectPHIDs',
array());
$any_user_project_phids = $query->getParameter(
'anyUserProjectPHIDs',
array());
$exclude_project_phids = $query->getParameter(
'excludeProjectPHIDs',
array());
$low_priority = $query->getParameter('lowPriority');
$high_priority = $query->getParameter('highPriority');
$page_size = $query->getParameter('limit');
$page = $query->getParameter('offset');
$q_status = $query->getParameter('status');
$q_group = $query->getParameter('group');
$q_order = $query->getParameter('order');
$form = id(new AphrontFormView())
->setUser($user)
->setAction(
$request->getRequestURI()
->alter('key', null)
->alter(
$this->getStatusRequestKey(),
$this->getStatusRequestValue($q_status))
->alter(
$this->getOrderRequestKey(),
$this->getOrderRequestValue($q_order))
->alter(
$this->getGroupRequestKey(),
$this->getGroupRequestValue($q_group)));
if (isset($has_filter[$this->view])) {
$tokens = array();
foreach ($user_phids as $phid) {
$tokens[$phid] = $handles[$phid]->getFullName();
}
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/searchowner/')
->setName('set_users')
->setLabel(pht('Users'))
->setValue($tokens));
}
if ($this->view == 'custom') {
$form->appendChild(
id(new AphrontFormTextControl())
->setName('set_search')
->setLabel(pht('Search'))
->setValue($search_text));
$form->appendChild(
id(new AphrontFormTextControl())
->setName('set_tasks')
->setLabel(pht('Task IDs'))
->setValue(join(',', $task_ids)));
$tokens = array();
foreach ($owner_phids as $phid) {
$tokens[$phid] = $handles[$phid]->getFullName();
}
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/searchowner/')
->setName('set_owners')
->setLabel(pht('Owners'))
->setValue($tokens));
$tokens = array();
foreach ($author_phids as $phid) {
$tokens[$phid] = $handles[$phid]->getFullName();
}
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/authors/')
->setName('set_authors')
->setLabel(pht('Authors'))
->setValue($tokens));
}
$tokens = array();
foreach ($project_phids as $phid) {
$tokens[$phid] = $handles[$phid]->getFullName();
}
if ($this->view != 'projectall' && $this->view != 'projecttriage') {
$caption = null;
if ($this->view == 'custom') {
$caption = pht('Find tasks in ALL of these projects ("AND" query).');
}
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/searchproject/')
->setName('set_projects')
->setLabel(pht('Projects'))
->setCaption($caption)
->setValue($tokens));
}
if ($this->view == 'custom') {
$atokens = array();
foreach ($any_project_phids as $phid) {
$atokens[$phid] = $handles[$phid]->getFullName();
}
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/projects/')
->setName('set_aprojects')
->setLabel(pht('Any Projects'))
->setCaption(pht('Find tasks in ANY of these projects ("OR" query).'))
->setValue($atokens));
$tokens = array();
foreach ($any_user_project_phids as $phid) {
$tokens[$phid] = $handles[$phid]->getFullName();
}
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/users/')
->setName('set_useraprojects')
->setLabel(pht('Any User Projects'))
->setCaption(
pht('Find tasks in ANY of these users\' projects ("OR" query).'))
->setValue($tokens));
$tokens = array();
foreach ($exclude_project_phids as $phid) {
$tokens[$phid] = $handles[$phid]->getFullName();
}
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/projects/')
->setName('set_xprojects')
->setLabel(pht('Exclude Projects'))
->setCaption(pht('Find tasks NOT in any of these projects.'))
->setValue($tokens));
$priority = ManiphestTaskPriority::getLowestPriority();
if ($low_priority !== null) {
$priority = $low_priority;
}
$form->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Min Priority'))
->setName('set_lpriority')
->setValue($priority)
->setOptions(array_reverse(
ManiphestTaskPriority::getTaskPriorityMap(), true)));
$priority = ManiphestTaskPriority::getHighestPriority();
if ($high_priority !== null) {
$priority = $high_priority;
}
$form->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Max Priority'))
->setName('set_hpriority')
->setValue($priority)
->setOptions(ManiphestTaskPriority::getTaskPriorityMap()));
}
$form
->appendChild($this->renderStatusControl($q_status))
->appendChild($this->renderGroupControl($q_group))
->appendChild($this->renderOrderControl($q_order));
$submit = id(new AphrontFormSubmitControl())
->setValue(pht('Filter Tasks'));
// Only show "Save..." for novel queries which have some kind of query
// parameters set.
if ($this->view === 'custom'
&& empty($key)
&& $request->getRequestURI()->getQueryParams()) {
$submit->addCancelButton(
'/maniphest/custom/edit/?key='.$query->getQueryKey(),
pht('Save Custom Query...'));
}
$form->appendChild($submit);
$create_uri = new PhutilURI('/maniphest/task/create/');
if ($project_phids) {
// If we have project filters selected, use them as defaults for task
// creation.
$create_uri->setQueryParam('projects', implode(';', $project_phids));
}
$filter = new AphrontListFilterView();
if (empty($key)) {
$filter->appendChild($form);
}
$have_tasks = false;
foreach ($tasks as $group => $list) {
if (count($list)) {
$have_tasks = true;
break;
}
}
require_celerity_resource('maniphest-task-summary-css');
$list_container = new AphrontNullView();
$list_container->appendChild(hsprintf(
'<div class="maniphest-list-container">'));
if (!$have_tasks) {
$no_tasks = pht('No matching tasks.');
$list_container->appendChild(hsprintf(
'<h1 class="maniphest-task-group-header">'.
'%s'.
'</h1>',
$no_tasks));
$result_count = null;
} else {
$pager = new AphrontPagerView();
$pager->setURI($request->getRequestURI(), 'offset');
$pager->setPageSize($page_size);
$pager->setOffset($page);
$pager->setCount($total_count);
$cur = ($pager->getOffset() + 1);
$max = min($pager->getOffset() + $page_size, $total_count);
$tot = $total_count;
$results = pht('Displaying tasks %s - %s of %s.',
number_format($cur),
number_format($max),
number_format($tot));
$result_count = phutil_tag(
'div',
array(
'class' => 'maniphest-total-result-count'
),
$results);
$selector = new AphrontNullView();
$group = $query->getParameter('group');
$order = $query->getParameter('order');
$is_draggable =
($order == 'priority') &&
($group == 'none' || $group == 'priority');
$lists = array();
foreach ($tasks as $group => $list) {
$task_list = new ManiphestTaskListView();
$task_list->setShowBatchControls(true);
if ($is_draggable) {
$task_list->setShowSubpriorityControls(true);
}
$task_list->setUser($user);
$task_list->setTasks($list);
$task_list->setHandles($handles);
$count = number_format(count($list));
$header =
javelin_tag(
'h1',
array(
'class' => 'maniphest-task-group-header',
'sigil' => 'task-group',
'meta' => array(
'priority' => head($list)->getPriority(),
),
),
$group.' ('.$count.')');
$lists[] =
phutil_tag(
'div',
array(
'class' => 'maniphest-task-group'
),
array(
$header,
$task_list,
));
}
$selector->appendChild($lists);
$selector->appendChild($this->renderBatchEditor($query));
$list_container->appendChild($selector);
$list_container->appendChild($pager);
Javelin::initBehavior(
'maniphest-subpriority-editor',
array(
'uri' => '/maniphest/subpriority/',
));
}
$nav->appendChild($filter);
$nav->appendChild($result_count);
$nav->appendChild($list_container);
$title = pht('Task List');
$crumbs = $this->buildApplicationCrumbs()
->addCrumb(
id(new PhabricatorCrumbView())
->setName($title))
->addAction(
id(new PHUIListItemView())
->setHref($this->getApplicationURI('/task/create/'))
->setName(pht('Create Task'))
->setIcon('create'));
$nav->setCrumbs($crumbs);
return $this->buildApplicationPage(
$nav,
array(
'title' => $title,
'device' => true,
));
}
public static function loadTasks(
PhabricatorSearchQuery $search_query,
PhabricatorUser $viewer) {
$any_project = false;
$search_text = $search_query->getParameter('fullTextSearch');
$user_phids = $search_query->getParameter('userPHIDs', array());
$task_ids = $search_query->getParameter('taskIDs', array());
$project_phids = $search_query->getParameter('projectPHIDs', array());
$any_project_phids = $search_query->getParameter(
'anyProjectPHIDs',
array());
$any_user_project_phids = $search_query->getParameter(
'anyUserProjectPHIDs',
array());
$xproject_phids = $search_query->getParameter(
'excludeProjectPHIDs',
array());
$owner_phids = $search_query->getParameter('ownerPHIDs', array());
$author_phids = $search_query->getParameter('authorPHIDs', array());
$low_priority = $search_query->getParameter('lowPriority');
$low_priority = coalesce($low_priority,
ManiphestTaskPriority::getLowestPriority());
$high_priority = $search_query->getParameter('highPriority');
$high_priority = coalesce($high_priority,
ManiphestTaskPriority::getHighestPriority());
$query = new ManiphestTaskQuery();
$query->setViewer($viewer);
$query->withIDs($task_ids);
if ($project_phids) {
$query->withAllProjects($project_phids);
}
if ($xproject_phids) {
$query->withoutProjects($xproject_phids);
}
if ($any_project_phids) {
$query->withAnyProjects($any_project_phids);
}
if ($owner_phids) {
$query->withOwners($owner_phids);
}
if ($author_phids) {
$query->withAuthors($author_phids);
}
if ($any_user_project_phids) {
$query->setViewer($viewer);
$query->withAnyUserProjects($any_user_project_phids);
}
$status = $search_query->getParameter('status', 'all');
if (!empty($status['open']) && !empty($status['closed'])) {
$query->withStatus(ManiphestTaskQuery::STATUS_ANY);
} else if (!empty($status['open'])) {
$query->withStatus(ManiphestTaskQuery::STATUS_OPEN);
} else {
$query->withStatus(ManiphestTaskQuery::STATUS_CLOSED);
}
switch ($search_query->getParameter('view')) {
case 'action':
$query->withOwners($user_phids);
break;
case 'created':
$query->withAuthors($user_phids);
break;
case 'subscribed':
$query->withSubscribers($user_phids);
break;
case 'triage':
$query->withOwners($user_phids);
$query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE);
break;
case 'alltriage':
$query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE);
break;
case 'all':
break;
case 'projecttriage':
$query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE);
break;
case 'projectall':
break;
case 'custom':
$query->withPrioritiesBetween($low_priority, $high_priority);
break;
}
$query->withFullTextSearch($search_text);
$order_map = array(
'priority' => ManiphestTaskQuery::ORDER_PRIORITY,
'created' => ManiphestTaskQuery::ORDER_CREATED,
'title' => ManiphestTaskQuery::ORDER_TITLE,
);
$query->setOrderBy(
idx(
$order_map,
$search_query->getParameter('order'),
ManiphestTaskQuery::ORDER_MODIFIED));
$group_map = array(
'priority' => ManiphestTaskQuery::GROUP_PRIORITY,
'owner' => ManiphestTaskQuery::GROUP_OWNER,
'status' => ManiphestTaskQuery::GROUP_STATUS,
'project' => ManiphestTaskQuery::GROUP_PROJECT,
);
$query->setGroupBy(
idx(
$group_map,
$search_query->getParameter('group'),
ManiphestTaskQuery::GROUP_NONE));
$query->setCalculateRows(true);
$query->setLimit($search_query->getParameter('limit'));
$query->setOffset($search_query->getParameter('offset'));
$data = $query->execute();
$total_row_count = $query->getRowCount();
$project_group_phids = array();
if ($search_query->getParameter('group') == 'project') {
foreach ($data as $task) {
foreach ($task->getProjectPHIDs() as $phid) {
$project_group_phids[] = $phid;
}
}
}
$handle_phids = mpull($data, 'getOwnerPHID');
$handle_phids = array_merge(
$handle_phids,
$project_phids,
$user_phids,
$xproject_phids,
$owner_phids,
$author_phids,
$project_group_phids,
$any_project_phids,
$any_user_project_phids,
array_mergev(mpull($data, 'getProjectPHIDs')));
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($handle_phids)
->execute();
switch ($search_query->getParameter('group')) {
case 'priority':
$data = mgroup($data, 'getPriority');
// If we have invalid priorities, they'll all map to "???". Merge
// arrays to prevent them from overwriting each other.
$out = array();
foreach ($data as $pri => $tasks) {
$out[ManiphestTaskPriority::getTaskPriorityName($pri)][] = $tasks;
}
foreach ($out as $pri => $tasks) {
$out[$pri] = array_mergev($tasks);
}
$data = $out;
break;
case 'status':
$data = mgroup($data, 'getStatus');
$out = array();
foreach ($data as $status => $tasks) {
$out[ManiphestTaskStatus::getTaskStatusFullName($status)] = $tasks;
}
$data = $out;
break;
case 'owner':
$data = mgroup($data, 'getOwnerPHID');
$out = array();
foreach ($data as $phid => $tasks) {
if ($phid) {
$out[$handles[$phid]->getFullName()] = $tasks;
} else {
$out['Unassigned'] = $tasks;
}
}
$data = $out;
ksort($data);
// Move "Unassigned" to the top of the list.
if (isset($data['Unassigned'])) {
$data = array('Unassigned' => $out['Unassigned']) + $out;
}
break;
case 'project':
- $grouped = array();
- foreach ($query->getGroupByProjectResults() as $project => $tasks) {
- foreach ($tasks as $task) {
- $group = 'No Project';
- if ($project && isset($handles[$project])) {
- $group = $handles[$project]->getName();
+ $data = mgroup($data, 'getGroupByProjectPHID');
+
+ $out = array();
+ foreach ($data as $phid => $tasks) {
+ $name = pht('No Project');
+ if ($phid) {
+ $handle = idx($handles, $phid);
+ if ($handle) {
+ $name = $handles[$phid]->getFullName();
}
- $grouped[$group][$task->getID()] = $task;
}
+ $out[$name] = $tasks;
}
- $data = $grouped;
- ksort($data);
-
- // Move "No Project" to the end of the list.
- if (isset($data['No Project'])) {
- $noproject = $data['No Project'];
- unset($data['No Project']);
- $data += array('No Project' => $noproject);
- }
+ $data = $out;
break;
default:
$data = array(
'Tasks' => $data,
);
break;
}
return array($data, $handles, $total_row_count);
}
private function renderBatchEditor(PhabricatorSearchQuery $search_query) {
$user = $this->getRequest()->getUser();
Javelin::initBehavior(
'maniphest-batch-selector',
array(
'selectAll' => 'batch-select-all',
'selectNone' => 'batch-select-none',
'submit' => 'batch-select-submit',
'status' => 'batch-select-status-cell',
'idContainer' => 'batch-select-id-container',
'formID' => 'batch-select-form',
));
$select_all = javelin_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'class' => 'grey button',
'id' => 'batch-select-all',
),
pht('Select All'));
$select_none = javelin_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'class' => 'grey button',
'id' => 'batch-select-none',
),
pht('Clear Selection'));
$submit = phutil_tag(
'button',
array(
'id' => 'batch-select-submit',
'disabled' => 'disabled',
'class' => 'disabled',
),
pht("Batch Edit Selected \xC2\xBB"));
$export = javelin_tag(
'a',
array(
'href' => '/maniphest/export/'.$search_query->getQueryKey().'/',
'class' => 'grey button',
),
pht('Export to Excel'));
$hidden = phutil_tag(
'div',
array(
'id' => 'batch-select-id-container',
),
'');
$editor = hsprintf(
'<div class="maniphest-batch-editor">'.
'<div class="batch-editor-header">%s</div>'.
'<table class="maniphest-batch-editor-layout">'.
'<tr>'.
'<td>%s%s</td>'.
'<td>%s</td>'.
'<td id="batch-select-status-cell">%s</td>'.
'<td class="batch-select-submit-cell">%s%s</td>'.
'</tr>'.
'</table>'.
'</div>',
pht('Batch Task Editor'),
$select_all,
$select_none,
$export,
'',
$submit,
$hidden);
$editor = phabricator_form(
$user,
array(
'method' => 'POST',
'action' => '/maniphest/batch/',
'id' => 'batch-select-form',
),
$editor);
return $editor;
}
private function buildQueryFromRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$status = $this->getStatusValueFromRequest();
$group = $this->getGroupValueFromRequest();
$order = $this->getOrderValueFromRequest();
$user_phids = $request->getStrList(
'users',
array($user->getPHID()));
if ($this->view == 'projecttriage' || $this->view == 'projectall') {
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withMemberPHIDs($user_phids)
->execute();
$any_project_phids = mpull($projects, 'getPHID');
$any_user_project_phids = array();
} else {
$any_project_phids = $request->getStrList('aprojects');
$any_user_project_phids = $request->getStrList('useraprojects');
}
$project_phids = $request->getStrList('projects');
$exclude_project_phids = $request->getStrList('xprojects');
$task_ids = $request->getStrList('tasks');
if ($task_ids) {
// We only need the integer portion of each task ID, so get rid of any
// non-numeric elements
$numeric_task_ids = array();
foreach ($task_ids as $task_id) {
$task_id = preg_replace('/\D+/', '', $task_id);
if (!empty($task_id)) {
$numeric_task_ids[] = $task_id;
}
}
if (empty($numeric_task_ids)) {
$numeric_task_ids = array(null);
}
$task_ids = $numeric_task_ids;
}
$owner_phids = $request->getStrList('owners');
$author_phids = $request->getStrList('authors');
$search_string = $request->getStr('search');
$low_priority = $request->getInt('lpriority');
$high_priority = $request->getInt('hpriority');
$page = $request->getInt('offset');
$page_size = self::DEFAULT_PAGE_SIZE;
$query = new PhabricatorSearchQuery();
$query->setQuery('<<maniphest>>');
$query->setParameters(
array(
'fullTextSearch' => $search_string,
'view' => $this->view,
'userPHIDs' => $user_phids,
'projectPHIDs' => $project_phids,
'anyProjectPHIDs' => $any_project_phids,
'anyUserProjectPHIDs' => $any_user_project_phids,
'excludeProjectPHIDs' => $exclude_project_phids,
'ownerPHIDs' => $owner_phids,
'authorPHIDs' => $author_phids,
'taskIDs' => $task_ids,
'lowPriority' => $low_priority,
'highPriority' => $high_priority,
'group' => $group,
'order' => $order,
'offset' => $page,
'limit' => $page_size,
'status' => $status,
));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$query->save();
unset($unguarded);
return $query;
}
/* -( Toggle Button Controls )---------------------------------------------
These are a giant mess since we have several different values: the request
key (GET param used in requests), the request value (short names used in
requests to keep URIs readable), and the query value (complex value stored in
the query).
*/
private function getStatusValueFromRequest() {
$map = $this->getStatusMap();
$val = $this->getRequest()->getStr($this->getStatusRequestKey());
return idx($map, $val, head($map));
}
private function getGroupValueFromRequest() {
$map = $this->getGroupMap();
$val = $this->getRequest()->getStr($this->getGroupRequestKey());
return idx($map, $val, head($map));
}
private function getOrderValueFromRequest() {
$map = $this->getOrderMap();
$val = $this->getRequest()->getStr($this->getOrderRequestKey());
return idx($map, $val, head($map));
}
private function getStatusRequestKey() {
return 's';
}
private function getGroupRequestKey() {
return 'g';
}
private function getOrderRequestKey() {
return 'o';
}
private function getStatusRequestValue($value) {
return array_search($value, $this->getStatusMap());
}
private function getGroupRequestValue($value) {
return array_search($value, $this->getGroupMap());
}
private function getOrderRequestValue($value) {
return array_search($value, $this->getOrderMap());
}
private function getStatusMap() {
return array(
'o' => array(
'open' => true,
),
'c' => array(
'closed' => true,
),
'oc' => array(
'open' => true,
'closed' => true,
),
);
}
private function getGroupMap() {
return array(
'p' => 'priority',
'o' => 'owner',
's' => 'status',
'j' => 'project',
'n' => 'none',
);
}
private function getOrderMap() {
return array(
'p' => 'priority',
'u' => 'updated',
'c' => 'created',
't' => 'title',
);
}
private function getStatusButtonMap() {
return array(
'o' => pht('Open'),
'c' => pht('Closed'),
'oc' => pht('All'),
);
}
private function getGroupButtonMap() {
return array(
'p' => pht('Priority'),
'o' => pht('Owner'),
's' => pht('Status'),
'j' => pht('Project'),
'n' => pht('None'),
);
}
private function getOrderButtonMap() {
return array(
'p' => pht('Priority'),
'u' => pht('Updated'),
'c' => pht('Created'),
't' => pht('Title'),
);
}
public function renderStatusControl($value) {
$request = $this->getRequest();
return id(new AphrontFormToggleButtonsControl())
->setLabel(pht('Status'))
->setValue($this->getStatusRequestValue($value))
->setBaseURI($request->getRequestURI(), $this->getStatusRequestKey())
->setButtons($this->getStatusButtonMap());
}
public function renderOrderControl($value) {
$request = $this->getRequest();
return id(new AphrontFormToggleButtonsControl())
->setLabel(pht('Order'))
->setValue($this->getOrderRequestValue($value))
->setBaseURI($request->getRequestURI(), $this->getOrderRequestKey())
->setButtons($this->getOrderButtonMap());
}
public function renderGroupControl($value) {
$request = $this->getRequest();
return id(new AphrontFormToggleButtonsControl())
->setLabel(pht('Group'))
->setValue($this->getGroupRequestValue($value))
->setBaseURI($request->getRequestURI(), $this->getGroupRequestKey())
->setButtons($this->getGroupButtonMap());
}
}
diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php
index d76314d111..637a5cc512 100644
--- a/src/applications/maniphest/query/ManiphestTaskQuery.php
+++ b/src/applications/maniphest/query/ManiphestTaskQuery.php
@@ -1,756 +1,743 @@
<?php
/**
* Query tasks by specific criteria. This class uses the higher-performance
* but less-general Maniphest indexes to satisfy queries.
*
* @group maniphest
*/
final class ManiphestTaskQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $taskIDs = array();
private $taskPHIDs = array();
private $authorPHIDs = array();
private $ownerPHIDs = array();
private $includeUnowned = null;
private $projectPHIDs = array();
private $xprojectPHIDs = array();
private $subscriberPHIDs = array();
private $anyProjectPHIDs = array();
private $anyUserProjectPHIDs = array();
private $includeNoProject = null;
private $dateCreatedAfter;
private $dateCreatedBefore;
private $fullTextSearch = '';
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_RESOLVED = 'status-resolved';
const STATUS_WONTFIX = 'status-wontfix';
const STATUS_INVALID = 'status-invalid';
const STATUS_SPITE = 'status-spite';
const STATUS_DUPLICATE = 'status-duplicate';
private $statuses;
private $priority = null;
private $priorities;
private $minPriority = null;
private $maxPriority = null;
private $groupBy = 'group-none';
const GROUP_NONE = 'group-none';
const GROUP_PRIORITY = 'group-priority';
const GROUP_OWNER = 'group-owner';
const GROUP_STATUS = 'group-status';
const GROUP_PROJECT = 'group-project';
private $orderBy = 'order-modified';
const ORDER_PRIORITY = 'order-priority';
const ORDER_CREATED = 'order-created';
const ORDER_MODIFIED = 'order-modified';
const ORDER_TITLE = 'order-title';
private $limit = null;
const DEFAULT_PAGE_SIZE = 1000;
private $offset = 0;
private $calculateRows = false;
private $rowCount = null;
- private $groupByProjectResults = null; // See comment at bottom for details
-
public function withAuthors(array $authors) {
$this->authorPHIDs = $authors;
return $this;
}
public function withIDs(array $ids) {
$this->taskIDs = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->taskPHIDs = $phids;
return $this;
}
public function withOwners(array $owners) {
$this->includeUnowned = false;
foreach ($owners as $k => $phid) {
if ($phid == ManiphestTaskOwner::OWNER_UP_FOR_GRABS || $phid === null) {
$this->includeUnowned = true;
unset($owners[$k]);
break;
}
}
$this->ownerPHIDs = $owners;
return $this;
}
public function withAllProjects(array $projects) {
$this->includeNoProject = false;
foreach ($projects as $k => $phid) {
if ($phid == ManiphestTaskOwner::PROJECT_NO_PROJECT) {
$this->includeNoProject = true;
unset($projects[$k]);
}
}
$this->projectPHIDs = $projects;
return $this;
}
public function withoutProjects(array $projects) {
$this->xprojectPHIDs = $projects;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withPriority($priority) {
$this->priority = $priority;
return $this;
}
public function withPriorities(array $priorities) {
$this->priorities = $priorities;
return $this;
}
public function withPrioritiesBetween($min, $max) {
$this->minPriority = $min;
$this->maxPriority = $max;
return $this;
}
public function withSubscribers(array $subscribers) {
$this->subscriberPHIDs = $subscribers;
return $this;
}
public function withFullTextSearch($fulltext_search) {
$this->fullTextSearch = $fulltext_search;
return $this;
}
public function setGroupBy($group) {
$this->groupBy = $group;
return $this;
}
public function setOrderBy($order) {
$this->orderBy = $order;
return $this;
}
public function setCalculateRows($calculate_rows) {
$this->calculateRows = $calculate_rows;
return $this;
}
public function getRowCount() {
if ($this->rowCount === null) {
throw new Exception(
"You must execute a query with setCalculateRows() before you can ".
"retrieve a row count.");
}
return $this->rowCount;
}
- public function getGroupByProjectResults() {
- return $this->groupByProjectResults;
- }
-
public function withAnyProjects(array $projects) {
$this->anyProjectPHIDs = $projects;
return $this;
}
public function withAnyUserProjects(array $users) {
$this->anyUserProjectPHIDs = $users;
return $this;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
public function loadPage() {
// TODO: (T603) It is possible for a user to find the PHID of a project
// they can't see, then query for tasks in that project and deduce the
// identity of unknown/invisible projects. Before we allow the user to
// execute a project-based PHID query, we should verify that they
// can see the project.
$task_dao = new ManiphestTask();
$conn = $task_dao->establishConnection('r');
if ($this->calculateRows) {
$calc = 'SQL_CALC_FOUND_ROWS';
// Make sure we end up in the right state if we throw a
// PhabricatorEmptyQueryException.
$this->rowCount = 0;
} else {
$calc = '';
}
$where = array();
$where[] = $this->buildTaskIDsWhereClause($conn);
$where[] = $this->buildTaskPHIDsWhereClause($conn);
$where[] = $this->buildStatusWhereClause($conn);
$where[] = $this->buildStatusesWhereClause($conn);
$where[] = $this->buildPriorityWhereClause($conn);
$where[] = $this->buildPrioritiesWhereClause($conn);
$where[] = $this->buildAuthorWhereClause($conn);
$where[] = $this->buildOwnerWhereClause($conn);
$where[] = $this->buildSubscriberWhereClause($conn);
$where[] = $this->buildProjectWhereClause($conn);
$where[] = $this->buildAnyProjectWhereClause($conn);
$where[] = $this->buildAnyUserProjectWhereClause($conn);
$where[] = $this->buildXProjectWhereClause($conn);
$where[] = $this->buildFullTextWhereClause($conn);
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn,
'dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn,
'dateCreated <= %d',
$this->dateCreatedBefore);
}
$where = $this->formatWhereClause($where);
- $join = array();
- $join[] = $this->buildProjectJoinClause($conn);
- $join[] = $this->buildAnyProjectJoinClause($conn);
- $join[] = $this->buildXProjectJoinClause($conn);
- $join[] = $this->buildSubscriberJoinClause($conn);
-
- $join = array_filter($join);
- if ($join) {
- $join = implode(' ', $join);
- } else {
- $join = '';
- }
-
$having = '';
$count = '';
- $group = '';
-
- if (count($this->projectPHIDs) > 1 || count($this->anyProjectPHIDs) > 1) {
- // If we're joining multiple rows, we need to group the results by the
- // task IDs.
- $group = 'GROUP BY task.id';
- } else {
- $group = '';
- }
if (count($this->projectPHIDs) > 1) {
// We want to treat the query as an intersection query, not a union
// query. We sum the project count and require it be the same as the
// number of projects we're searching for.
$count = ', COUNT(project.projectPHID) projectCount';
$having = qsprintf(
$conn,
'HAVING projectCount = %d',
count($this->projectPHIDs));
}
$order = $this->buildCustomOrderClause($conn);
// TODO: Clean up this nonstandardness.
if (!$this->getLimit()) {
$this->setLimit(self::DEFAULT_PAGE_SIZE);
}
- if ($this->groupBy == self::GROUP_PROJECT) {
- $this->setLimit(PHP_INT_MAX);
- $this->setOffset(0);
+ $group_column = '';
+ switch ($this->groupBy) {
+ case self::GROUP_PROJECT:
+ $group_column = qsprintf(
+ $conn,
+ ', projectGroupName.indexedObjectPHID projectGroupPHID');
+ break;
}
- $data = queryfx_all(
+ $rows = queryfx_all(
$conn,
- 'SELECT %Q * %Q FROM %T task %Q %Q %Q %Q %Q %Q',
+ 'SELECT %Q task.* %Q %Q FROM %T task %Q %Q %Q %Q %Q %Q',
$calc,
$count,
+ $group_column,
$task_dao->getTableName(),
- $join,
+ $this->buildJoinsClause($conn),
$where,
- $group,
+ $this->buildGroupClause($conn),
$having,
$order,
$this->buildLimitClause($conn));
if ($this->calculateRows) {
$count = queryfx_one(
$conn,
'SELECT FOUND_ROWS() N');
$this->rowCount = $count['N'];
} else {
$this->rowCount = null;
}
+ switch ($this->groupBy) {
+ case self::GROUP_PROJECT:
+ $data = ipull($rows, null, 'id');
+ break;
+ default:
+ $data = $rows;
+ break;
+ }
+
$tasks = $task_dao->loadAllFromArray($data);
- if ($this->groupBy == self::GROUP_PROJECT) {
- $tasks = $this->applyGroupByProject($tasks);
+ switch ($this->groupBy) {
+ case self::GROUP_PROJECT:
+ $results = array();
+ foreach ($rows as $row) {
+ $task = clone $tasks[$row['id']];
+ $task->attachGroupByProjectPHID($row['projectGroupPHID']);
+ $results[] = $task;
+ }
+ $tasks = $results;
+ break;
}
return $tasks;
}
private function buildTaskIDsWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->taskIDs) {
return null;
}
return qsprintf(
$conn,
'id in (%Ld)',
$this->taskIDs);
}
private function buildTaskPHIDsWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->taskPHIDs) {
return null;
}
return qsprintf(
$conn,
'phid in (%Ls)',
$this->taskPHIDs);
}
private function buildStatusWhereClause(AphrontDatabaseConnection $conn) {
static $map = array(
self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE,
);
switch ($this->status) {
case self::STATUS_ANY:
return null;
case self::STATUS_OPEN:
return 'status = 0';
case self::STATUS_CLOSED:
return 'status > 0';
default:
$constant = idx($map, $this->status);
if (!$constant) {
throw new Exception("Unknown status query '{$this->status}'!");
}
return qsprintf(
$conn,
'status = %d',
$constant);
}
}
private function buildStatusesWhereClause(AphrontDatabaseConnection $conn) {
if ($this->statuses) {
return qsprintf(
$conn,
'status IN (%Ld)',
$this->statuses);
}
return null;
}
private function buildPriorityWhereClause(AphrontDatabaseConnection $conn) {
if ($this->priority !== null) {
return qsprintf(
$conn,
'priority = %d',
$this->priority);
} elseif ($this->minPriority !== null && $this->maxPriority !== null) {
return qsprintf(
$conn,
'priority >= %d AND priority <= %d',
$this->minPriority,
$this->maxPriority);
}
return null;
}
private function buildPrioritiesWhereClause(AphrontDatabaseConnection $conn) {
if ($this->priorities) {
return qsprintf(
$conn,
'priority IN (%Ld)',
$this->priorities);
}
return null;
}
private function buildAuthorWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->authorPHIDs) {
return null;
}
return qsprintf(
$conn,
'authorPHID in (%Ls)',
$this->authorPHIDs);
}
private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->ownerPHIDs) {
if ($this->includeUnowned === null) {
return null;
} else if ($this->includeUnowned) {
return qsprintf(
$conn,
'ownerPHID IS NULL');
} else {
return qsprintf(
$conn,
'ownerPHID IS NOT NULL');
}
}
if ($this->includeUnowned) {
return qsprintf(
$conn,
'ownerPHID IN (%Ls) OR ownerPHID IS NULL',
$this->ownerPHIDs);
} else {
return qsprintf(
$conn,
'ownerPHID IN (%Ls)',
$this->ownerPHIDs);
}
}
private function buildFullTextWhereClause(AphrontDatabaseConnection $conn) {
if (!strlen($this->fullTextSearch)) {
return null;
}
// In doing a fulltext search, we first find all the PHIDs that match the
// fulltext search, and then use that to limit the rest of the search
$fulltext_query = new PhabricatorSearchQuery();
$fulltext_query->setQuery($this->fullTextSearch);
$fulltext_query->setParameter('limit', PHP_INT_MAX);
$fulltext_query->setParameter('type', ManiphestPHIDTypeTask::TYPECONST);
$engine = PhabricatorSearchEngineSelector::newSelector()->newEngine();
$fulltext_results = $engine->executeSearch($fulltext_query);
if (empty($fulltext_results)) {
$fulltext_results = array(null);
}
return qsprintf(
$conn,
'phid IN (%Ls)',
$fulltext_results);
}
private function buildSubscriberWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->subscriberPHIDs) {
return null;
}
return qsprintf(
$conn,
'subscriber.subscriberPHID IN (%Ls)',
$this->subscriberPHIDs);
}
private function buildProjectWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->projectPHIDs && !$this->includeNoProject) {
return null;
}
$parts = array();
if ($this->projectPHIDs) {
$parts[] = qsprintf(
$conn,
'project.projectPHID in (%Ls)',
$this->projectPHIDs);
}
if ($this->includeNoProject) {
$parts[] = qsprintf(
$conn,
'project.projectPHID IS NULL');
}
return '('.implode(') OR (', $parts).')';
}
- private function buildProjectJoinClause(AphrontDatabaseConnection $conn) {
- if (!$this->projectPHIDs && !$this->includeNoProject) {
- return null;
- }
-
- $project_dao = new ManiphestTaskProject();
- return qsprintf(
- $conn,
- '%Q JOIN %T project ON project.taskPHID = task.phid',
- ($this->includeNoProject ? 'LEFT' : ''),
- $project_dao->getTableName());
- }
-
private function buildAnyProjectWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->anyProjectPHIDs) {
return null;
}
return qsprintf(
$conn,
'anyproject.projectPHID IN (%Ls)',
$this->anyProjectPHIDs);
}
private function buildAnyUserProjectWhereClause(
AphrontDatabaseConnection $conn) {
if (!$this->anyUserProjectPHIDs) {
return null;
}
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withMemberPHIDs($this->anyUserProjectPHIDs)
->execute();
$any_user_project_phids = mpull($projects, 'getPHID');
if (!$any_user_project_phids) {
throw new PhabricatorEmptyQueryException();
}
return qsprintf(
$conn,
'anyproject.projectPHID IN (%Ls)',
$any_user_project_phids);
}
- private function buildAnyProjectJoinClause(AphrontDatabaseConnection $conn) {
- if (!$this->anyProjectPHIDs && !$this->anyUserProjectPHIDs) {
- return null;
- }
-
- $project_dao = new ManiphestTaskProject();
- return qsprintf(
- $conn,
- 'JOIN %T anyproject ON anyproject.taskPHID = task.phid',
- $project_dao->getTableName());
- }
-
private function buildXProjectWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->xprojectPHIDs) {
return null;
}
return qsprintf(
$conn,
'xproject.projectPHID IS NULL');
}
- private function buildXProjectJoinClause(AphrontDatabaseConnection $conn) {
- if (!$this->xprojectPHIDs) {
- return null;
- }
-
- $project_dao = new ManiphestTaskProject();
- return qsprintf(
- $conn,
- 'LEFT JOIN %T xproject ON xproject.taskPHID = task.phid
- AND xproject.projectPHID IN (%Ls)',
- $project_dao->getTableName(),
- $this->xprojectPHIDs);
- }
-
- private function buildSubscriberJoinClause(AphrontDatabaseConnection $conn) {
- if (!$this->subscriberPHIDs) {
- return null;
- }
-
- $subscriber_dao = new ManiphestTaskSubscriber();
- return qsprintf(
- $conn,
- 'JOIN %T subscriber ON subscriber.taskPHID = task.phid',
- $subscriber_dao->getTableName());
- }
-
private function buildCustomOrderClause(AphrontDatabaseConnection $conn) {
$order = array();
switch ($this->groupBy) {
case self::GROUP_NONE:
break;
case self::GROUP_PRIORITY:
$order[] = 'priority';
break;
case self::GROUP_OWNER:
$order[] = 'ownerOrdering';
break;
case self::GROUP_STATUS:
$order[] = 'status';
break;
case self::GROUP_PROJECT:
- // NOTE: We have to load the entire result set and apply this grouping
- // in the PHP process for now.
+ $order[] = '<group.project>';
break;
default:
throw new Exception("Unknown group query '{$this->groupBy}'!");
}
switch ($this->orderBy) {
case self::ORDER_PRIORITY:
$order[] = 'priority';
$order[] = 'subpriority';
$order[] = 'dateModified';
break;
case self::ORDER_CREATED:
$order[] = 'id';
break;
case self::ORDER_MODIFIED:
$order[] = 'dateModified';
break;
case self::ORDER_TITLE:
$order[] = 'title';
break;
default:
throw new Exception("Unknown order query '{$this->orderBy}'!");
}
$order = array_unique($order);
if (empty($order)) {
return null;
}
foreach ($order as $k => $column) {
switch ($column) {
case 'subpriority':
case 'ownerOrdering':
case 'title':
$order[$k] = "task.{$column} ASC";
break;
+ case '<group.project>':
+ // Put "No Project" at the end of the list.
+ $order[$k] =
+ 'projectGroupName.indexedObjectName IS NULL ASC, '.
+ 'projectGroupName.indexedObjectName ASC';
+ break;
default:
$order[$k] = "task.{$column} DESC";
break;
}
}
return 'ORDER BY '.implode(', ', $order);
}
+ private function buildJoinsClause(AphrontDatabaseConnection $conn_r) {
+ $project_dao = new ManiphestTaskProject();
- /**
- * To get paging to work for "group by project", we need to do a bunch of
- * server-side magic since there's currently no way to sort by project name on
- * the database.
- *
- * As a consequence of this, moreover, because the list we return from here
- * may include a single task multiple times (once for each project it's in),
- * sorting gets screwed up in the controller unless we tell it which project
- * to put the task in each time it appears. Hence the magic field
- * groupByProjectResults.
- *
- * TODO: Move this all to the database.
- */
- private function applyGroupByProject(array $tasks) {
- assert_instances_of($tasks, 'ManiphestTask');
+ $joins = array();
- $project_phids = array();
- foreach ($tasks as $task) {
- foreach ($task->getProjectPHIDs() as $phid) {
- $project_phids[$phid] = true;
- }
+ if ($this->projectPHIDs || $this->includeNoProject) {
+ $joins[] = qsprintf(
+ $conn_r,
+ '%Q JOIN %T project ON project.taskPHID = task.phid',
+ ($this->includeNoProject ? 'LEFT' : ''),
+ $project_dao->getTableName());
}
- $handles = id(new PhabricatorHandleQuery())
- ->setViewer($this->getViewer())
- ->withPHIDs(array_keys($project_phids))
- ->execute();
+ if ($this->anyProjectPHIDs || $this->anyUserProjectPHIDs) {
+ $joins[] = qsprintf(
+ $conn_r,
+ 'JOIN %T anyproject ON anyproject.taskPHID = task.phid',
+ $project_dao->getTableName());
+ }
- $max = 1;
- foreach ($handles as $handle) {
- $max = max($max, strlen($handle->getName()));
+ if ($this->xprojectPHIDs) {
+ $joins[] = qsprintf(
+ $conn_r,
+ 'LEFT JOIN %T xproject ON xproject.taskPHID = task.phid
+ AND xproject.projectPHID IN (%Ls)',
+ $project_dao->getTableName(),
+ $this->xprojectPHIDs);
}
- $items = array();
- $ii = 0;
- foreach ($tasks as $key => $task) {
- $phids = $task->getProjectPHIDs();
- if ($this->projectPHIDs) {
- $phids = array_diff($phids, $this->projectPHIDs);
- }
- if ($phids) {
- foreach ($phids as $phid) {
- $items[] = array(
- 'key' => $key,
- 'proj' => $phid,
- 'seq' => sprintf(
- '%'.$max.'s%09d',
- $handles[$phid]->getName(),
- $ii),
- );
+ if ($this->subscriberPHIDs) {
+ $subscriber_dao = new ManiphestTaskSubscriber();
+ $joins[] = qsprintf(
+ $conn_r,
+ 'JOIN %T subscriber ON subscriber.taskPHID = task.phid',
+ $subscriber_dao->getTableName());
+ }
+
+ switch ($this->groupBy) {
+ case self::GROUP_PROJECT:
+ $ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs();
+ if ($ignore_group_phids) {
+ $joins[] = qsprintf(
+ $conn_r,
+ 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.taskPHID
+ AND projectGroup.projectPHID NOT IN (%Ls)',
+ $project_dao->getTableName(),
+ $ignore_group_phids);
+ } else {
+ $joins[] = qsprintf(
+ $conn_r,
+ 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.taskPHID',
+ $project_dao->getTableName());
}
+ $joins[] = qsprintf(
+ $conn_r,
+ 'LEFT JOIN %T projectGroupName
+ ON projectGroup.projectPHID = projectGroupName.indexedObjectPHID',
+ id(new ManiphestNameIndex())->getTableName());
+ break;
+ }
+
+ return implode(' ', $joins);
+ }
+
+ private function buildGroupClause(AphrontDatabaseConnection $conn_r) {
+ $joined_multiple_project_rows = (count($this->projectPHIDs) > 1) ||
+ (count($this->anyProjectPHIDs) > 1);
+
+ $joined_project_name = ($this->groupBy == self::GROUP_PROJECT);
+
+ // If we're joining multiple rows, we need to group the results by the
+ // task IDs.
+ if ($joined_multiple_project_rows) {
+ if ($joined_project_name) {
+ return 'GROUP BY task.id, projectGroup.projectPHID';
} else {
- // Sort "no project" tasks first.
- $items[] = array(
- 'key' => $key,
- 'proj' => null,
- 'seq' => sprintf(
- '%'.$max.'s%09d',
- '',
- $ii),
- );
+ return 'GROUP BY task.id';
}
- ++$ii;
+ } else {
+ return '';
}
+ }
+
+ /**
+ * Return project PHIDs which we should ignore when grouping tasks by
+ * project. For example, if a user issues a query like:
+ *
+ * Tasks in all projects: Frontend, Bugs
+ *
+ * ...then we don't show "Frontend" or "Bugs" groups in the result set, since
+ * they're meaningless as all results are in both groups.
+ *
+ * Similarly, for queries like:
+ *
+ * Tasks in any projects: Public Relations
+ *
+ * ...we ignore the single project, as every result is in that project. (In
+ * the case that there are several "any" projects, we do not ignore them.)
+ *
+ * @return list<phid> Project PHIDs which should be ignored in query
+ * construction.
+ */
+ private function getIgnoreGroupedProjectPHIDs() {
+ $phids = array();
- $items = isort($items, 'seq');
- $items = array_slice(
- $items,
- nonempty($this->getOffset()),
- nonempty($this->getLimit(), self::DEFAULT_PAGE_SIZE));
+ if ($this->projectPHIDs) {
+ $phids[] = $this->projectPHIDs;
+ }
- $result = array();
- $projects = array();
- foreach ($items as $item) {
- $result[] = $projects[$item['proj']][] = $tasks[$item['key']];
+ if (count($this->anyProjectPHIDs) == 1) {
+ $phids[] = $this->anyProjectPHIDs;
}
- $this->groupByProjectResults = $projects;
- return $result;
+ // Maybe we should also exclude the "excludeProjectPHIDs"? It won't
+ // impact the results, but we might end up with a better query plan.
+ // Investigate this on real data? This is likely very rare.
+
+ return array_mergev($phids);
}
+
}
diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php
index 3ac3f68962..5d3cdeb77e 100644
--- a/src/applications/maniphest/storage/ManiphestTask.php
+++ b/src/applications/maniphest/storage/ManiphestTask.php
@@ -1,298 +1,308 @@
<?php
/**
* @group maniphest
*/
final class ManiphestTask extends ManiphestDAO
implements
PhabricatorMarkupInterface,
PhabricatorPolicyInterface,
PhabricatorTokenReceiverInterface,
PhrequentTrackableInterface {
const MARKUP_FIELD_DESCRIPTION = 'markup:desc';
protected $phid;
protected $authorPHID;
protected $ownerPHID;
protected $ccPHIDs = array();
protected $status;
protected $priority;
protected $subpriority;
protected $title;
protected $originalTitle;
protected $description;
protected $originalEmailSource;
protected $mailKey;
protected $attached = array();
protected $projectPHIDs = array();
private $projectsNeedUpdate;
private $subscribersNeedUpdate;
protected $ownerOrdering;
private $auxiliaryAttributes = self::ATTACHABLE;
private $auxiliaryDirty = array();
+ private $groupByProjectPHID = self::ATTACHABLE;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'ccPHIDs' => self::SERIALIZATION_JSON,
'attached' => self::SERIALIZATION_JSON,
'projectPHIDs' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function loadDependsOnTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK);
}
public function loadDependedOnByTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK);
}
public function getAttachedPHIDs($type) {
return array_keys(idx($this->attached, $type, array()));
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(ManiphestPHIDTypeTask::TYPECONST);
}
public function getCCPHIDs() {
return array_values(nonempty($this->ccPHIDs, array()));
}
public function setProjectPHIDs(array $phids) {
$this->projectPHIDs = array_values($phids);
$this->projectsNeedUpdate = true;
return $this;
}
public function getProjectPHIDs() {
return array_values(nonempty($this->projectPHIDs, array()));
}
public function setCCPHIDs(array $phids) {
$this->ccPHIDs = array_values($phids);
$this->subscribersNeedUpdate = true;
return $this;
}
public function setOwnerPHID($phid) {
$this->ownerPHID = nonempty($phid, null);
$this->subscribersNeedUpdate = true;
return $this;
}
public function getAuxiliaryAttribute($key, $default = null) {
$this->assertAttached($this->auxiliaryAttributes);
return idx($this->auxiliaryAttributes, $key, $default);
}
public function setAuxiliaryAttribute($key, $val) {
$this->assertAttached($this->auxiliaryAttributes);
$this->auxiliaryAttributes[$key] = $val;
$this->auxiliaryDirty[$key] = true;
return $this;
}
public function setTitle($title) {
$this->title = $title;
if (!$this->getID()) {
$this->originalTitle = $title;
}
return $this;
}
+ public function attachGroupByProjectPHID($phid) {
+ $this->groupByProjectPHID = $phid;
+ return $this;
+ }
+
+ public function getGroupByProjectPHID() {
+ return $this->assertAttached($this->groupByProjectPHID);
+ }
+
public function attachAuxiliaryAttributes(array $attrs) {
if ($this->auxiliaryDirty) {
throw new Exception(
"This object has dirty attributes, you can not attach new attributes ".
"without writing or discarding the dirty attributes.");
}
$this->auxiliaryAttributes = $attrs;
return $this;
}
public function loadAndAttachAuxiliaryAttributes() {
if (!$this->getPHID()) {
$this->auxiliaryAttributes = array();
return $this;
}
$storage = id(new ManiphestTaskAuxiliaryStorage())->loadAllWhere(
'taskPHID = %s',
$this->getPHID());
$this->auxiliaryAttributes = mpull($storage, 'getValue', 'getName');
return $this;
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
$result = parent::save();
if ($this->projectsNeedUpdate) {
// If we've changed the project PHIDs for this task, update the link
// table.
ManiphestTaskProject::updateTaskProjects($this);
$this->projectsNeedUpdate = false;
}
if ($this->subscribersNeedUpdate) {
// If we've changed the subscriber PHIDs for this task, update the link
// table.
ManiphestTaskSubscriber::updateTaskSubscribers($this);
$this->subscribersNeedUpdate = false;
}
if ($this->auxiliaryDirty) {
$this->writeAuxiliaryUpdates();
$this->auxiliaryDirty = array();
}
return $result;
}
private function writeAuxiliaryUpdates() {
$table = new ManiphestTaskAuxiliaryStorage();
$conn_w = $table->establishConnection('w');
$update = array();
$remove = array();
foreach ($this->auxiliaryDirty as $key => $dirty) {
$value = $this->getAuxiliaryAttribute($key);
if ($value === null) {
$remove[$key] = true;
} else {
$update[$key] = $value;
}
}
if ($remove) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE taskPHID = %s AND name IN (%Ls)',
$table->getTableName(),
$this->getPHID(),
array_keys($remove));
}
if ($update) {
$sql = array();
foreach ($update as $key => $val) {
$sql[] = qsprintf(
$conn_w,
'(%s, %s, %s, %d, %d)',
$this->getPHID(),
$key,
$val,
time(),
time());
}
queryfx(
$conn_w,
'INSERT INTO %T (taskPHID, name, value, dateCreated, dateModified)
VALUES %Q ON DUPLICATE KEY
UPDATE value = VALUES(value), dateModified = VALUES(dateModified)',
$table->getTableName(),
implode(', ', $sql));
}
}
/* -( Markup Interface )--------------------------------------------------- */
/**
* @task markup
*/
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "maniphest:T{$id}:{$field}:{$hash}";
}
/**
* @task markup
*/
public function getMarkupText($field) {
return $this->getDescription();
}
/**
* @task markup
*/
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newManiphestMarkupEngine();
}
/**
* @task markup
*/
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
/**
* @task markup
*/
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
/* -( Policy Interface )--------------------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_USER;
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return false;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
// Sort of ambiguous who this was intended for; just let them both know.
return array_filter(
array_unique(
array(
$this->getAuthorPHID(),
$this->getOwnerPHID(),
)));
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jul 29, 2:22 AM (2 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
187871
Default Alt Text
(63 KB)

Event Timeline