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 @@ 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( '
')); if (!$have_tasks) { $no_tasks = pht('No matching tasks.'); $list_container->appendChild(hsprintf( '

'. '%s'. '

', $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( '
'. '
%s
'. ''. ''. ''. ''. ''. ''. ''. '
%s%s%s%s%s%s
'. '
', 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('<>'); $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 @@ 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[] = ''; 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 '': + // 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 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 @@ 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(), ))); } }