From de0c89261e9ea789af9c54b72d01437cb043c467 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 29 Jun 2011 16:16:33 -0700 Subject: [PATCH] Allow Maniphest tasks to be filtered by Project Summary: Major things taking place here: - A new table for storing relationships. - Moved all task query logic into a dedicated class. - Added a "projects" filter to the UI. I was originally going to try to drive this off the main search index but the perf benefits of a custom schema make an overwhelming argument in favor of doing it this way. Test Plan: Filtered tasks by author and owner and zero, one, and more than one project. Exercised all the group/sort options. Ran the index script over my 100k task corpus. Edited task-project membership and verified the index updated. Reviewed By: cadamo Reviewers: gc3, jungejason, cadamo, tuomaspelkonen, aran CC: aran, cadamo, epriestley Differential Revision: 556 --- CHANGELOG | 5 + resources/sql/patches/051.projectfilter.sql | 6 + scripts/search/reindex_maniphest.php | 32 ++ src/__phutil_library_map__.php | 3 + .../tasklist/ManiphestTaskListController.php | 222 +++++------ .../controller/tasklist/__init__.php | 5 +- .../maniphest/query/ManiphestTaskQuery.php | 351 ++++++++++++++++++ src/applications/maniphest/query/__init__.php | 18 + .../maniphest/storage/task/ManiphestTask.php | 19 +- .../maniphest/storage/task/__init__.php | 1 + .../taskproject/ManiphestTaskProject.php | 67 ++++ .../storage/taskproject/__init__.php | 14 + 12 files changed, 610 insertions(+), 133 deletions(-) create mode 100644 resources/sql/patches/051.projectfilter.sql create mode 100755 scripts/search/reindex_maniphest.php create mode 100644 src/applications/maniphest/query/ManiphestTaskQuery.php create mode 100644 src/applications/maniphest/query/__init__.php create mode 100644 src/applications/maniphest/storage/taskproject/ManiphestTaskProject.php create mode 100644 src/applications/maniphest/storage/taskproject/__init__.php diff --git a/CHANGELOG b/CHANGELOG index 09b5bdd0a9..171beb5130 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,11 @@ This is not a complete list of changes, just of API or workflow changes that may break existing installs. Newer changes are listed at the top. If you pull new changes and things stop working, check here first! +June 29 2011 - Maniphest project indexes + Old Maniphest tasks will not appear in project filter views until you run + "scripts/search/reindex_maniphest.php" to build indexes. New tasks will have + their indexes built automatically. + May 31 2011 - Javelin submodule moved The externals/javelin submodule location has moved. If you have an older checkout of Phabricator, you may need to edit .git/config to point at diff --git a/resources/sql/patches/051.projectfilter.sql b/resources/sql/patches/051.projectfilter.sql new file mode 100644 index 0000000000..42554d01a7 --- /dev/null +++ b/resources/sql/patches/051.projectfilter.sql @@ -0,0 +1,6 @@ +CREATE TABLE phabricator_maniphest.maniphest_taskproject ( + taskPHID varchar(64) BINARY NOT NULL, + projectPHID varchar(64) BINARY NOT NULL, + PRIMARY KEY (projectPHID, taskPHID), + UNIQUE KEY (taskPHID, projectPHID) +); diff --git a/scripts/search/reindex_maniphest.php b/scripts/search/reindex_maniphest.php new file mode 100755 index 0000000000..62550d56d2 --- /dev/null +++ b/scripts/search/reindex_maniphest.php @@ -0,0 +1,32 @@ +#!/usr/bin/env php +loadAll(); +echo "Updating relationships for ".count($tasks)." tasks"; +foreach ($tasks as $task) { + ManiphestTaskProject::updateTaskProjects($task); + echo '.'; +} +echo "\nDone.\n"; + diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 271fcc39f5..d050187ca1 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -272,6 +272,8 @@ phutil_register_library_map(array( 'ManiphestTaskListView' => 'applications/maniphest/view/tasklist', 'ManiphestTaskOwner' => 'applications/maniphest/constants/owner', 'ManiphestTaskPriority' => 'applications/maniphest/constants/priority', + 'ManiphestTaskProject' => 'applications/maniphest/storage/taskproject', + 'ManiphestTaskQuery' => 'applications/maniphest/query', 'ManiphestTaskStatus' => 'applications/maniphest/constants/status', 'ManiphestTaskSummaryView' => 'applications/maniphest/view/tasksummary', 'ManiphestTransaction' => 'applications/maniphest/storage/transaction', @@ -765,6 +767,7 @@ phutil_register_library_map(array( 'ManiphestTaskEditController' => 'ManiphestController', 'ManiphestTaskListController' => 'ManiphestController', 'ManiphestTaskListView' => 'AphrontView', + 'ManiphestTaskProject' => 'ManiphestDAO', 'ManiphestTaskSummaryView' => 'AphrontView', 'ManiphestTransaction' => 'ManiphestDAO', 'ManiphestTransactionDetailView' => 'AphrontView', diff --git a/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php b/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php index 72250e2d1a..0c0dee7d1f 100644 --- a/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php +++ b/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php @@ -33,23 +33,30 @@ class ManiphestTaskListController extends ManiphestController { $uri = $request->getRequestURI(); if ($request->isFormPost()) { - $phid_arr = $request->getArr('view_user'); - $view_target = head($phid_arr); - return id(new AphrontRedirectResponse()) - ->setURI($request->getRequestURI()->alter('phid', $view_target)); - } + // Redirect to GET so URIs can be copy/pasted. + $user_phids = $request->getArr('set_users'); + $proj_phids = $request->getArr('set_projects'); + $user_phids = implode(',', $user_phids); + $proj_phids = implode(',', $proj_phids); + $user_phids = nonempty($user_phids, null); + $proj_phids = nonempty($proj_phids, null); + + $uri = $request->getRequestURI() + ->alter('users', $user_phids) + ->alter('projects', $proj_phids); + + return id(new AphrontRedirectResponse())->setURI($uri); + } $views = array( 'User Tasks', 'action' => 'Assigned', 'created' => 'Created', 'triage' => 'Need Triage', -// 'touched' => 'Touched', '
', 'All Tasks', 'alltriage' => 'Need Triage', - 'unassigned' => 'Unassigned', 'all' => 'All Tasks', ); @@ -77,7 +84,7 @@ class ManiphestTaskListController extends ManiphestController { phutil_render_tag( 'a', array( - 'href' => $uri, + 'href' => $uri->alter('page', null), 'class' => ($this->view == $view) ? 'aphront-side-nav-selected' : null, @@ -90,13 +97,26 @@ class ManiphestTaskListController extends ManiphestController { list($grouping, $group_links) = $this->renderGroupLinks(); list($order, $order_links) = $this->renderOrderLinks(); - $view_phid = nonempty($request->getStr('phid'), $user->getPHID()); + $user_phids = $request->getStr('users'); + if (strlen($user_phids)) { + $user_phids = explode(',', $user_phids); + } else { + $user_phids = array($user->getPHID()); + } + + $project_phids = $request->getStr('projects'); + if (strlen($project_phids)) { + $project_phids = explode(',', $project_phids); + } else { + $project_phids = array(); + } $page = $request->getInt('page'); $page_size = self::DEFAULT_PAGE_SIZE; list($tasks, $handles, $total_count) = $this->loadTasks( - $view_phid, + $user_phids, + $project_phids, array( 'status' => $status_map, 'group' => $grouping, @@ -105,24 +125,34 @@ class ManiphestTaskListController extends ManiphestController { 'limit' => $page_size, )); - $form = id(new AphrontFormView()) - ->setUser($user); + ->setUser($user) + ->setAction($request->getRequestURI()); if (isset($has_filter[$this->view])) { + $tokens = array(); + foreach ($user_phids as $phid) { + $tokens[$phid] = $handles[$phid]->getFullName(); + } $form->appendChild( id(new AphrontFormTokenizerControl()) - ->setLimit(1) ->setDatasource('/typeahead/common/searchowner/') - ->setName('view_user') - ->setLabel('View User') - ->setCaption('Use "upforgrabs" to find unassigned tasks.') - ->setValue( - array( - $view_phid => $handles[$view_phid]->getFullName(), - ))); + ->setName('set_users') + ->setLabel('Users') + ->setValue($tokens)); } + $tokens = array(); + foreach ($project_phids as $phid) { + $tokens[$phid] = $handles[$phid]->getFullName(); + } + $form->appendChild( + id(new AphrontFormTokenizerControl()) + ->setDatasource('/typeahead/common/projects/') + ->setName('set_projects') + ->setLabel('Projects') + ->setValue($tokens)); + $form ->appendChild( id(new AphrontFormToggleButtonsControl()) @@ -137,6 +167,10 @@ class ManiphestTaskListController extends ManiphestController { ->setLabel('Order') ->setValue($order_links)); + $form->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Filter Tasks')); + $filter = new AphrontListFilterView(); $filter->addButton( phutil_render_tag( @@ -209,141 +243,71 @@ class ManiphestTaskListController extends ManiphestController { )); } - private function loadTasks($view_phid, array $dict) { - $phids = array($view_phid); + private function loadTasks( + array $user_phids, + array $project_phids, + array $dict) { - $include_upforgrabs = false; - foreach ($phids as $key => $phid) { - if ($phid == ManiphestTaskOwner::OWNER_UP_FOR_GRABS) { - unset($phids[$key]); - $include_upforgrabs = true; - } - } - - $task = new ManiphestTask(); - - $argv = array(); + $query = new ManiphestTaskQuery(); + $query->withProjects($project_phids); $status = $dict['status']; if (!empty($status['open']) && !empty($status['closed'])) { - $status_clause = '1 = 1'; + $query->withStatus(ManiphestTaskQuery::STATUS_ANY); } else if (!empty($status['open'])) { - $status_clause = 'status = %d'; - $argv[] = 0; + $query->withStatus(ManiphestTaskQuery::STATUS_OPEN); } else { - $status_clause = 'status > %d'; - $argv[] = 0; + $query->withStatus(ManiphestTaskQuery::STATUS_CLOSED); } - $extra_clause = '1 = 1'; switch ($this->view) { case 'action': - $parts = array(); - if ($phids) { - $parts[] = 'ownerPHID in (%Ls)'; - $argv[] = $phids; - } - if ($include_upforgrabs) { - $parts[] = 'ownerPHID IS NULL'; - } - $extra_clause = '('.implode(' OR ', $parts).')'; + $query->withOwners($user_phids); break; case 'created': - $parts = array(); - if ($phids) { - $parts[] = 'authorPHID in (%Ls)'; - $argv[] = $phids; - } - if ($include_upforgrabs) { - // This should be impossible since every task is supposed to have a - // valid author, but we might as well run the query. - $parts[] = 'authorPHID IS NULL'; - } - $extra_clause = '('.implode(' OR ', $parts).')'; + $query->withAuthors($user_phids); break; case 'triage': - $parts = array(); - if ($phids) { - $parts[] = 'ownerPHID in (%Ls)'; - $argv[] = $phids; - } - if ($include_upforgrabs) { - $parts[] = 'ownerPHID IS NULL'; - } - $extra_clause = '('.implode(' OR ', $parts).') AND priority = %d'; - $argv[] = ManiphestTaskPriority::PRIORITY_TRIAGE; + $query->withOwners($user_phids); + $query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE); break; case 'alltriage': - $extra_clause = 'priority = %d'; - $argv[] = ManiphestTaskPriority::PRIORITY_TRIAGE; - break; - case 'unassigned': - $extra_clause = 'ownerPHID is NULL'; + $query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE); break; case 'all': break; } - $order = array(); - switch ($dict['group']) { - case 'priority': - $order[] = 'priority'; - break; - case 'owner': - $order[] = 'ownerOrdering'; - break; - case 'status': - $order[] = 'status'; - break; - } + $order_map = array( + 'priority' => ManiphestTaskQuery::ORDER_PRIORITY, + 'created' => ManiphestTaskQuery::ORDER_CREATED, + ); + $query->setOrderBy( + idx( + $order_map, + $dict['order'], + ManiphestTaskQuery::ORDER_MODIFIED)); - switch ($dict['order']) { - case 'priority': - $order[] = 'priority'; - $order[] = 'dateModified'; - break; - case 'created': - $order[] = 'id'; - break; - default: - $order[] = 'dateModified'; - break; - } + $group_map = array( + 'priority' => ManiphestTaskQuery::GROUP_PRIORITY, + 'owner' => ManiphestTaskQuery::GROUP_OWNER, + 'status' => ManiphestTaskQuery::GROUP_STATUS, + ); + $query->setGroupBy( + idx( + $group_map, + $dict['group'], + ManiphestTaskQuery::GROUP_NONE)); - $order = array_unique($order); + $query->setCalculateRows(true); + $query->setLimit($dict['limit']); + $query->setOffset($dict['offset']); - foreach ($order as $k => $column) { - switch ($column) { - case 'ownerOrdering': - $order[$k] = "{$column} ASC"; - break; - default: - $order[$k] = "{$column} DESC"; - break; - } - } - - $order = implode(', ', $order); - - $offset = (int)idx($dict, 'offset', 0); - $limit = (int)idx($dict, 'limit', self::DEFAULT_PAGE_SIZE); - - $sql = "SELECT SQL_CALC_FOUND_ROWS * FROM %T WHERE ". - "({$status_clause}) AND ({$extra_clause}) ORDER BY {$order} ". - "LIMIT {$offset}, {$limit}"; - - array_unshift($argv, $task->getTableName()); - - $conn = $task->establishConnection('r'); - $data = vqueryfx_all($conn, $sql, $argv); - - $total_row_count = queryfx_one($conn, 'SELECT FOUND_ROWS() N'); - $total_row_count = $total_row_count['N']; - - $data = $task->loadAllFromArray($data); + $data = $query->execute(); + $total_row_count = $query->getRowCount(); $handle_phids = mpull($data, 'getOwnerPHID'); - $handle_phids[] = $view_phid; + $handle_phids = array_merge($handle_phids, $project_phids, $user_phids); $handles = id(new PhabricatorObjectHandleData($handle_phids)) ->loadHandles(); diff --git a/src/applications/maniphest/controller/tasklist/__init__.php b/src/applications/maniphest/controller/tasklist/__init__.php index 16e6296085..28c7ec5a4c 100644 --- a/src/applications/maniphest/controller/tasklist/__init__.php +++ b/src/applications/maniphest/controller/tasklist/__init__.php @@ -7,17 +7,16 @@ phutil_require_module('phabricator', 'aphront/response/redirect'); -phutil_require_module('phabricator', 'applications/maniphest/constants/owner'); phutil_require_module('phabricator', 'applications/maniphest/constants/priority'); phutil_require_module('phabricator', 'applications/maniphest/constants/status'); phutil_require_module('phabricator', 'applications/maniphest/controller/base'); -phutil_require_module('phabricator', 'applications/maniphest/storage/task'); +phutil_require_module('phabricator', 'applications/maniphest/query'); phutil_require_module('phabricator', 'applications/maniphest/view/tasklist'); phutil_require_module('phabricator', 'applications/phid/handle/data'); phutil_require_module('phabricator', 'infrastructure/celerity/api'); -phutil_require_module('phabricator', 'storage/queryfx'); phutil_require_module('phabricator', 'view/control/pager'); phutil_require_module('phabricator', 'view/form/base'); +phutil_require_module('phabricator', 'view/form/control/submit'); phutil_require_module('phabricator', 'view/form/control/togglebuttons'); phutil_require_module('phabricator', 'view/form/control/tokenizer'); phutil_require_module('phabricator', 'view/layout/listfilter'); diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php new file mode 100644 index 0000000000..d7f874f1eb --- /dev/null +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -0,0 +1,351 @@ +authorPHIDs = $authors; + return $this; + } + + public function withOwners(array $owners) { + $this->includeUnowned = false; + foreach ($owners as $k => $phid) { + if ($phid == ManiphestTaskOwner::OWNER_UP_FOR_GRABS) { + $this->includeUnowned = true; + unset($owners[$k]); + break; + } + } + $this->ownerPHIDs = $owners; + return $this; + } + + public function withProjects(array $projects) { + $this->projectPHIDs = $projects; + return $this; + } + + public function withStatus($status) { + $this->status = $status; + return $this; + } + + public function withPriority($priority) { + $this->priority = $priority; + return $this; + } + + public function setGroupBy($group) { + $this->groupBy = $group; + return $this; + } + + public function setOrderBy($order) { + $this->orderBy = $order; + return $this; + } + + public function setLimit($limit) { + $this->limit = $limit; + return $this; + } + + public function setOffset($offset) { + $this->offset = $offset; + 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 execute() { + + $task_dao = new ManiphestTask(); + $conn = $task_dao->establishConnection('r'); + + if ($this->calculateRows) { + $calc = 'SQL_CALC_FOUND_ROWS'; + } else { + $calc = ''; + } + + $where = array(); + $where[] = $this->buildStatusWhereClause($conn); + $where[] = $this->buildPriorityWhereClause($conn); + $where[] = $this->buildAuthorWhereClause($conn); + $where[] = $this->buildOwnerWhereClause($conn); + $where[] = $this->buildProjectWhereClause($conn); + + $where = array_filter($where); + if ($where) { + $where = 'WHERE ('.implode(') AND (', $where).')'; + } else { + $where = ''; + } + + $join = array(); + $join[] = $this->buildProjectJoinClause($conn); + + $join = array_filter($join); + if ($join) { + $join = implode(' ', $join); + } else { + $join = ''; + } + + $having = ''; + $count = ''; + $group = ''; + if (count($this->projectPHIDs) > 1) { + + // If we're searching for more than one project: + // - We'll get multiple rows for tasks when they join the project table + // multiple times. We use GROUP BY to make them distinct again. + // - 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. + + $group = 'GROUP BY task.id'; + $count = ', COUNT(1) projectCount'; + $having = qsprintf( + $conn, + 'HAVING projectCount = %d', + count($this->projectPHIDs)); + } + + $order = $this->buildOrderClause($conn); + + $offset = (int)nonempty($this->offset, 0); + $limit = (int)nonempty($this->limit, self::DEFAULT_PAGE_SIZE); + + $data = queryfx_all( + $conn, + 'SELECT %Q * %Q FROM %T task %Q %Q %Q %Q %Q LIMIT %d, %d', + $calc, + $count, + $task_dao->getTableName(), + $join, + $where, + $group, + $having, + $order, + $offset, + $limit); + + if ($this->calculateRows) { + $count = queryfx_one( + $conn, + 'SELECT FOUND_ROWS() N'); + $this->rowCount = $count['N']; + } else { + $this->rowCount = null; + } + + return $task_dao->loadAllFromArray($data); + } + + private function buildStatusWhereClause($conn) { + switch ($this->status) { + case self::STATUS_ANY: + return null; + case self::STATUS_OPEN: + return 'status = 0'; + case self::STATUS_CLOSED: + return 'status > 0'; + default: + throw new Exception("Unknown status query '{$this->status}'!"); + } + } + + private function buildPriorityWhereClause($conn) { + if ($this->priority === null) { + return null; + } + + return qsprintf( + $conn, + 'priority = %d', + $this->priority); + } + + private function buildAuthorWhereClause($conn) { + if (!$this->authorPHIDs) { + return null; + } + + return qsprintf( + $conn, + 'authorPHID in (%Ls)', + $this->authorPHIDs); + } + + private function buildOwnerWhereClause($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 buildProjectWhereClause($conn) { + if (!$this->projectPHIDs) { + return null; + } + + return qsprintf( + $conn, + 'project.projectPHID IN (%Ls)', + $this->projectPHIDs); + } + + private function buildProjectJoinClause($conn) { + if (!$this->projectPHIDs) { + return null; + } + + $project_dao = new ManiphestTaskProject(); + return qsprintf( + $conn, + 'JOIN %T project ON project.taskPHID = task.phid', + $project_dao->getTableName()); + } + + private function buildOrderClause($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; + default: + throw new Exception("Unknown group query '{$this->groupBy}'!"); + } + + switch ($this->orderBy) { + case self::ORDER_PRIORITY: + $order[] = 'priority'; + $order[] = 'dateModified'; + break; + case self::ORDER_CREATED: + $order[] = 'id'; + break; + case self::ORDER_MODIFIED: + $order[] = 'dateModified'; + 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 'ownerOrdering': + $order[$k] = "task.{$column} ASC"; + break; + default: + $order[$k] = "task.{$column} DESC"; + break; + } + } + + return 'ORDER BY '.implode(', ', $order); + } + + +} diff --git a/src/applications/maniphest/query/__init__.php b/src/applications/maniphest/query/__init__.php new file mode 100644 index 0000000000..19b4f9fc8c --- /dev/null +++ b/src/applications/maniphest/query/__init__.php @@ -0,0 +1,18 @@ +ccPHIDs, array()); } + public function setProjectPHIDs(array $phids) { + $this->projectPHIDs = $phids; + $this->projectsNeedUpdate = true; + return $this; + } + public function save() { if (!$this->mailKey) { $this->mailKey = sha1(Filesystem::readRandomBytes(20)); } - return parent::save(); + + $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; + } + + return $result; } } diff --git a/src/applications/maniphest/storage/task/__init__.php b/src/applications/maniphest/storage/task/__init__.php index 22a5346f8c..14ba412113 100644 --- a/src/applications/maniphest/storage/task/__init__.php +++ b/src/applications/maniphest/storage/task/__init__.php @@ -7,6 +7,7 @@ phutil_require_module('phabricator', 'applications/maniphest/storage/base'); +phutil_require_module('phabricator', 'applications/maniphest/storage/taskproject'); phutil_require_module('phabricator', 'applications/phid/constants'); phutil_require_module('phabricator', 'applications/phid/storage/phid'); diff --git a/src/applications/maniphest/storage/taskproject/ManiphestTaskProject.php b/src/applications/maniphest/storage/taskproject/ManiphestTaskProject.php new file mode 100644 index 0000000000..687694934e --- /dev/null +++ b/src/applications/maniphest/storage/taskproject/ManiphestTaskProject.php @@ -0,0 +1,67 @@ + Project table, which denormalizes the + * relationship between tasks and projects into a link table so it can be + * efficiently queried. This table is not authoritative; the projectPHIDs field + * of ManiphestTask is. The rows in this table are regenerated when transactions + * are applied to tasks which affected their associated projects. + * + * @group maniphest + */ +final class ManiphestTaskProject extends ManiphestDAO { + + protected $taskPHID; + protected $projectPHID; + + public function getConfiguration() { + return array( + self::CONFIG_IDS => self::IDS_MANUAL, + self::CONFIG_TIMESTAMPS => false, + ); + } + + public static function updateTaskProjects(ManiphestTask $task) { + $dao = new ManiphestTaskProject(); + $conn = $dao->establishConnection('w'); + + $sql = array(); + foreach ($task->getProjectPHIDs() as $project_phid) { + $sql[] = qsprintf( + $conn, + '(%s, %s)', + $task->getPHID(), + $project_phid); + } + + queryfx( + $conn, + 'DELETE FROM %T WHERE taskPHID = %s', + $dao->getTableName(), + $task->getPHID()); + if ($sql) { + queryfx( + $conn, + 'INSERT INTO %T (taskPHID, projectPHID) VALUES %Q', + $dao->getTableName(), + implode(', ', $sql)); + } + } + +} diff --git a/src/applications/maniphest/storage/taskproject/__init__.php b/src/applications/maniphest/storage/taskproject/__init__.php new file mode 100644 index 0000000000..8a3c5b26af --- /dev/null +++ b/src/applications/maniphest/storage/taskproject/__init__.php @@ -0,0 +1,14 @@ +