Page MenuHomestyx hydra

No OneTemporary

diff --git a/resources/sql/patches/20130913.maniphest.1.migratesearch.php b/resources/sql/patches/20130913.maniphest.1.migratesearch.php
index 7cf51069cd..7f7be6d505 100644
--- a/resources/sql/patches/20130913.maniphest.1.migratesearch.php
+++ b/resources/sql/patches/20130913.maniphest.1.migratesearch.php
@@ -1,209 +1,212 @@
<?php
// NOTE: If you need to make any significant updates to this to deal with
// future changes to objects, it's probably better to just wipe the whole
// migration. This feature doesn't see overwhelming amounts of use, and users
// who do use it can recreate their queries fairly easily with the new
// interface. By the time this needs to be updated, the vast majority of
// users who it impacts will likely have migrated their data already.
$table = new ManiphestTask();
$conn_w = $table->establishConnection('w');
$search_table = new PhabricatorSearchQuery();
$search_conn_w = $search_table->establishConnection('w');
+// See T1812. This is an old status constant from the time of this migration.
+$old_open_status = 0;
+
echo "Updating saved Maniphest queries...\n";
$rows = new LiskRawMigrationIterator($conn_w, 'maniphest_savedquery');
foreach ($rows as $row) {
$id = $row['id'];
echo "Updating query {$id}...\n";
$data = queryfx_one(
$search_conn_w,
'SELECT parameters FROM %T WHERE queryKey = %s',
$search_table->getTableName(),
$row['queryKey']);
if (!$data) {
echo "Unable to locate query data.\n";
continue;
}
$data = json_decode($data['parameters'], true);
if (!is_array($data)) {
echo "Unable to decode query data.\n";
continue;
}
if (idx($data, 'view') != 'custom') {
echo "Query is not a custom query.\n";
continue;
}
$new_data = array(
'limit' => 1000,
);
if (isset($data['lowPriority']) || isset($data['highPriority'])) {
$lo = idx($data, 'lowPriority');
$hi = idx($data, 'highPriority');
$priorities = array();
$all = ManiphestTaskPriority::getTaskPriorityMap();
foreach ($all as $pri => $name) {
if (($lo !== null) && ($pri < $lo)) {
continue;
}
if (($hi !== null) && ($pri > $hi)) {
continue;
}
$priorities[] = $pri;
}
if (count($priorities) != count($all)) {
$new_data['priorities'] = $priorities;
}
}
foreach ($data as $key => $value) {
switch ($key) {
case 'fullTextSearch':
if (strlen($value)) {
$new_data['fulltext'] = $value;
}
break;
case 'userPHIDs':
// This was (I think?) one-off data provied to specific hard-coded
// queries.
break;
case 'projectPHIDs':
foreach ($value as $k => $v) {
if ($v === null || $v === ManiphestTaskOwner::PROJECT_NO_PROJECT) {
$new_data['withNoProject'] = true;
unset($value[$k]);
break;
}
}
if ($value) {
$new_data['allProjectPHIDs'] = $value;
}
break;
case 'anyProjectPHIDs':
if ($value) {
$new_data['anyProjectPHIDs'] = $value;
}
break;
case 'anyUserProjectPHIDs':
if ($value) {
$new_data['userProjectPHIDs'] = $value;
}
break;
case 'excludeProjectPHIDs':
if ($value) {
$new_data['excludeProjectPHIDs'] = $value;
}
break;
case 'ownerPHIDs':
foreach ($value as $k => $v) {
if ($v === null || $v === ManiphestTaskOwner::OWNER_UP_FOR_GRABS) {
$new_data['withUnassigned'] = true;
unset($value[$k]);
break;
}
}
if ($value) {
$new_data['assignedPHIDs'] = $value;
}
break;
case 'authorPHIDs':
if ($value) {
$new_data['authorPHIDs'] = $value;
}
break;
case 'taskIDs':
if ($value) {
$new_data['ids'] = $value;
}
break;
case 'status':
$include_open = !empty($value['open']);
$include_closed = !empty($value['closed']);
if ($include_open xor $include_closed) {
if ($include_open) {
$new_data['statuses'] = array(
- ManiphestTaskStatus::STATUS_OPEN,
+ $old_open_status,
);
} else {
$statuses = array();
foreach (ManiphestTaskStatus::getTaskStatusMap() as $status => $n) {
- if ($status != ManiphestTaskStatus::STATUS_OPEN) {
+ if ($status != $old_open_status) {
$statuses[] = $status;
}
}
$new_data['statuses'] = $statuses;
}
}
break;
case 'order':
$map = array(
'priority' => 'priority',
'updated' => 'updated',
'created' => 'created',
'title' => 'title',
);
if (isset($map[$value])) {
$new_data['order'] = $map[$value];
} else {
$new_data['order'] = 'priority';
}
break;
case 'group':
$map = array(
'priority' => 'priority',
'owner' => 'assigned',
'status' => 'status',
'project' => 'project',
'none' => 'none',
);
if (isset($map[$value])) {
$new_data['group'] = $map[$value];
} else {
$new_data['group'] = 'priority';
}
break;
}
}
$saved = id(new PhabricatorSavedQuery())
->setEngineClassName('ManiphestTaskSearchEngine');
foreach ($new_data as $key => $value) {
$saved->setParameter($key, $value);
}
try {
$saved->save();
} catch (AphrontQueryDuplicateKeyException $ex) {
// Ignore this, we just have duplicate saved queries.
}
$named = id(new PhabricatorNamedQuery())
->setEngineClassName('ManiphestTaskSearchEngine')
->setQueryKey($saved->getQueryKey())
->setQueryName($row['name'])
->setUserPHID($row['userPHID']);
try {
$named->save();
} catch (Exception $ex) {
// The user already has this query under another name. This can occur if
// the migration runs twice.
echo "Failed to save named query.\n";
continue;
}
echo "OK.\n";
}
echo "Done.\n";
diff --git a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php
index 8e56774502..a00dc7b9fb 100644
--- a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php
@@ -1,224 +1,192 @@
<?php
abstract class DifferentialFreeformFieldSpecification
extends DifferentialFieldSpecification {
private function findMentionedTasks($message) {
$maniphest = 'PhabricatorApplicationManiphest';
if (!PhabricatorApplication::isClassInstalled($maniphest)) {
return array();
}
- $prefixes = array(
- 'resolve' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
- 'resolves' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
- 'resolved' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
- 'fix' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
- 'fixes' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
- 'fixed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
- 'wontfix' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
- 'wontfixes' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
- 'wontfixed' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
- 'spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
- 'spites' => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
- 'spited' => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
- 'invalidate' => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
- 'invaldiates' => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
- 'invalidated' => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
- 'close' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
- 'closes' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
- 'closed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
- 'ref' => null,
- 'refs' => null,
- 'references' => null,
- 'cf.' => null,
- );
-
- $suffixes = array(
- 'as resolved' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
- 'as fixed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
- 'as wontfix' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
- 'as spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
- 'out of spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
- 'as invalid' => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
- '' => null,
- );
+ $prefixes = ManiphestTaskStatus::getStatusPrefixMap();
+ $suffixes = ManiphestTaskStatus::getStatusSuffixMap();
$matches = id(new ManiphestCustomFieldStatusParser())
->parseCorpus($message);
$task_statuses = array();
foreach ($matches as $match) {
$prefix = phutil_utf8_strtolower($match['prefix']);
$suffix = phutil_utf8_strtolower($match['suffix']);
$status = idx($suffixes, $suffix);
if (!$status) {
$status = idx($prefixes, $prefix);
}
foreach ($match['monograms'] as $task_monogram) {
$task_id = (int)trim($task_monogram, 'tT');
$task_statuses[$task_id] = $status;
}
}
return $task_statuses;
}
private function findDependentRevisions($message) {
$matches = id(new DifferentialCustomFieldDependsOnParser())
->parseCorpus($message);
$dependents = array();
foreach ($matches as $match) {
foreach ($match['monograms'] as $monogram) {
$id = (int)trim($monogram, 'dD');
$dependents[$id] = $id;
}
}
return $dependents;
}
public static function findRevertedCommits($message) {
$matches = id(new DifferentialCustomFieldRevertsParser())
->parseCorpus($message);
$result = array();
foreach ($matches as $match) {
foreach ($match['monograms'] as $monogram) {
$result[$monogram] = $monogram;
}
}
return $result;
}
public function didWriteRevision(DifferentialRevisionEditor $editor) {
$message = $this->renderValueForCommitMessage(false);
$tasks = $this->findMentionedTasks($message);
if ($tasks) {
$tasks = id(new ManiphestTaskQuery())
->setViewer($editor->getActor())
->withIDs(array_keys($tasks))
->execute();
$this->saveFieldEdges(
$editor->getRevision(),
PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK,
mpull($tasks, 'getPHID'));
}
$dependents = $this->findDependentRevisions($message);
if ($dependents) {
$dependents = id(new DifferentialRevisionQuery())
->setViewer($editor->getActor())
->withIDs($dependents)
->execute();
$this->saveFieldEdges(
$editor->getRevision(),
PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV,
mpull($dependents, 'getPHID'));
}
}
private function saveFieldEdges(
DifferentialRevision $revision,
$edge_type,
array $add_phids) {
$revision_phid = $revision->getPHID();
$old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$revision_phid,
$edge_type);
$add_phids = array_diff($add_phids, $old_phids);
if (!$add_phids) {
return;
}
$edge_editor = id(new PhabricatorEdgeEditor())->setActor($this->getUser());
foreach ($add_phids as $phid) {
$edge_editor->addEdge($revision_phid, $edge_type, $phid);
}
// NOTE: Deletes only through the fields.
$edge_editor->save();
}
public function didParseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data) {
$message = $this->renderValueForCommitMessage($is_edit = false);
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$data->getCommitDetail('authorPHID'));
if (!$user) {
// TODO: Maybe after grey users, we should find a way to proceed even
// if we don't know who the author is.
return;
}
$commit_names = self::findRevertedCommits($message);
if ($commit_names) {
$reverts = id(new DiffusionCommitQuery())
->setViewer($user)
->withIdentifiers($commit_names)
->withDefaultRepository($repository)
->execute();
foreach ($reverts as $revert) {
// TODO: Do interesting things here.
}
}
$tasks_statuses = $this->findMentionedTasks($message);
if (!$tasks_statuses) {
return;
}
$tasks = id(new ManiphestTaskQuery())
->setViewer($user)
->withIDs(array_keys($tasks_statuses))
->execute();
foreach ($tasks as $task_id => $task) {
id(new PhabricatorEdgeEditor())
->setActor($user)
->addEdge(
$task->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT,
$commit->getPHID())
->save();
$status = $tasks_statuses[$task_id];
if (!$status) {
// Text like "Ref T123", don't change the task status.
continue;
}
- if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) {
- // Task is already closed.
+ if ($task->getStatus() == $status) {
+ // Task is already in the specified status, so skip updating it.
continue;
}
$commit_name = $repository->formatCommitName(
$commit->getCommitIdentifier());
$call = new ConduitCall(
'maniphest.update',
array(
'id' => $task->getID(),
'status' => $status,
'comments' => "Closed by commit {$commit_name}.",
));
$call->setUser($user);
$call->execute();
}
}
}
diff --git a/src/applications/home/controller/PhabricatorHomeMainController.php b/src/applications/home/controller/PhabricatorHomeMainController.php
index bbd2bbc4d8..c3a60dddde 100644
--- a/src/applications/home/controller/PhabricatorHomeMainController.php
+++ b/src/applications/home/controller/PhabricatorHomeMainController.php
@@ -1,465 +1,465 @@
<?php
final class PhabricatorHomeMainController
extends PhabricatorHomeController {
private $filter;
private $minipanels = array();
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->filter = idx($data, 'filter');
}
public function processRequest() {
$user = $this->getRequest()->getUser();
if ($this->filter == 'jump') {
return $this->buildJumpResponse();
}
$nav = $this->buildNav();
$project_query = new PhabricatorProjectQuery();
$project_query->setViewer($user);
$project_query->withMemberPHIDs(array($user->getPHID()));
$projects = $project_query->execute();
return $this->buildMainResponse($nav, $projects);
}
private function buildMainResponse($nav, array $projects) {
assert_instances_of($projects, 'PhabricatorProject');
$maniphest = 'PhabricatorApplicationManiphest';
if (PhabricatorApplication::isClassInstalled($maniphest)) {
$unbreak_panel = $this->buildUnbreakNowPanel();
$triage_panel = $this->buildNeedsTriagePanel($projects);
$tasks_panel = $this->buildTasksPanel();
} else {
$unbreak_panel = null;
$triage_panel = null;
$tasks_panel = null;
}
$audit = 'PhabricatorApplicationAudit';
if (PhabricatorApplication::isClassInstalled($audit)) {
$audit_panel = $this->buildAuditPanel();
$commit_panel = $this->buildCommitPanel();
} else {
$audit_panel = null;
$commit_panel = null;
}
if (PhabricatorEnv::getEnvConfig('welcome.html') !== null) {
$welcome_panel = $this->buildWelcomePanel();
} else {
$welcome_panel = null;
}
$jump_panel = $this->buildJumpPanel();
$revision_panel = $this->buildRevisionPanel();
$content = array(
$jump_panel,
$welcome_panel,
$unbreak_panel,
$triage_panel,
$revision_panel,
$tasks_panel,
$audit_panel,
$commit_panel,
$this->minipanels,
);
$nav->appendChild($content);
$nav->appendChild(new PhabricatorGlobalUploadTargetView());
return $this->buildApplicationPage(
$nav,
array(
'title' => 'Phabricator',
'device' => true,
));
}
private function buildJumpResponse() {
$request = $this->getRequest();
$jump = $request->getStr('jump');
$response = PhabricatorJumpNavHandler::getJumpResponse(
$request->getUser(),
$jump);
if ($response) {
return $response;
} else if ($request->isFormPost()) {
$uri = new PhutilURI('/search/');
$uri->setQueryParam('query', $jump);
$uri->setQueryParam('search:primary', 'true');
return id(new AphrontRedirectResponse())->setURI((string)$uri);
} else {
return id(new AphrontRedirectResponse())->setURI('/');
}
}
private function buildUnbreakNowPanel() {
$unbreak_now = PhabricatorEnv::getEnvConfig(
'maniphest.priorities.unbreak-now');
if (!$unbreak_now) {
return null;
}
$user = $this->getRequest()->getUser();
$task_query = id(new ManiphestTaskQuery())
->setViewer($user)
- ->withStatuses(array(ManiphestTaskStatus::STATUS_OPEN))
+ ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants())
->withPriorities(array($unbreak_now))
->setLimit(10);
$tasks = $task_query->execute();
if (!$tasks) {
return $this->renderMiniPanel(
'No "Unbreak Now!" Tasks',
'Nothing appears to be critically broken right now.');
}
$href = '/maniphest/?statuses[]=0&priorities[]='.$unbreak_now.'#R';
$title = pht('Unbreak Now!');
$panel = new AphrontPanelView();
$panel->setHeader($this->renderSectionHeader($title, $href));
$panel->appendChild($this->buildTaskListView($tasks));
$panel->setNoBackground();
return $panel;
}
private function buildNeedsTriagePanel(array $projects) {
assert_instances_of($projects, 'PhabricatorProject');
$needs_triage = PhabricatorEnv::getEnvConfig(
'maniphest.priorities.needs-triage');
if (!$needs_triage) {
return null;
}
$user = $this->getRequest()->getUser();
if (!$user->isLoggedIn()) {
return null;
}
if ($projects) {
$task_query = id(new ManiphestTaskQuery())
->setViewer($user)
- ->withStatuses(array(ManiphestTaskStatus::STATUS_OPEN))
+ ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants())
->withPriorities(array($needs_triage))
->withAnyProjects(mpull($projects, 'getPHID'))
->setLimit(10);
$tasks = $task_query->execute();
} else {
$tasks = array();
}
if (!$tasks) {
return $this->renderMiniPanel(
'No "Needs Triage" Tasks',
hsprintf(
'No tasks in <a href="/project/">projects you are a member of</a> '.
'need triage.'));
}
$title = pht('Needs Triage');
$href = '/maniphest/?statuses[]=0&priorities[]='.$needs_triage.
'&userProjects[]='.$user->getPHID().'#R';
$panel = new AphrontPanelView();
$panel->setHeader($this->renderSectionHeader($title, $href));
$panel->appendChild($this->buildTaskListView($tasks));
$panel->setNoBackground();
return $panel;
}
private function buildRevisionPanel() {
$user = $this->getRequest()->getUser();
$user_phid = $user->getPHID();
$revision_query = id(new DifferentialRevisionQuery())
->setViewer($user)
->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
->withResponsibleUsers(array($user_phid))
->needRelationships(true);
$revisions = $revision_query->execute();
list($blocking, $active, ) = DifferentialRevisionQuery::splitResponsible(
$revisions,
array($user_phid));
if (!$blocking && !$active) {
return $this->renderMiniPanel(
'No Waiting Revisions',
'No revisions are waiting on you.');
}
$title = pht('Revisions Waiting on You');
$href = '/differential';
$panel = new AphrontPanelView();
$panel->setHeader($this->renderSectionHeader($title, $href));
$revision_view = id(new DifferentialRevisionListView())
->setHighlightAge(true)
->setRevisions(array_merge($blocking, $active))
->setFields(DifferentialRevisionListView::getDefaultFields($user))
->setUser($user)
->loadAssets();
$phids = array_merge(
array($user_phid),
$revision_view->getRequiredHandlePHIDs());
$handles = $this->loadViewerHandles($phids);
$revision_view->setHandles($handles);
$list_view = $revision_view->render();
$list_view->setFlush(true);
$panel->appendChild($list_view);
$panel->setNoBackground();
return $panel;
}
private function buildWelcomePanel() {
$panel = new AphrontPanelView();
$panel->appendChild(
phutil_safe_html(
PhabricatorEnv::getEnvConfig('welcome.html')));
$panel->setNoBackground();
return $panel;
}
private function buildTasksPanel() {
$user = $this->getRequest()->getUser();
$user_phid = $user->getPHID();
$task_query = id(new ManiphestTaskQuery())
->setViewer($user)
- ->withStatus(ManiphestTaskQuery::STATUS_OPEN)
+ ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants())
->setGroupBy(ManiphestTaskQuery::GROUP_PRIORITY)
->withOwners(array($user_phid))
->setLimit(10);
$tasks = $task_query->execute();
if (!$tasks) {
return $this->renderMiniPanel(
'No Assigned Tasks',
'You have no assigned tasks.');
}
$title = pht('Assigned Tasks');
$href = '/maniphest';
$panel = new AphrontPanelView();
$panel->setHeader($this->renderSectionHeader($title, $href));
$panel->appendChild($this->buildTaskListView($tasks));
$panel->setNoBackground();
return $panel;
}
private function buildTaskListView(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
$user = $this->getRequest()->getUser();
$phids = array_merge(
array_filter(mpull($tasks, 'getOwnerPHID')),
array_mergev(mpull($tasks, 'getProjectPHIDs')));
$handles = $this->loadViewerHandles($phids);
$view = new ManiphestTaskListView();
$view->setTasks($tasks);
$view->setUser($user);
$view->setHandles($handles);
return $view;
}
private function buildJumpPanel($query=null) {
$request = $this->getRequest();
$user = $request->getUser();
$uniq_id = celerity_generate_unique_node_id();
Javelin::initBehavior(
'phabricator-autofocus',
array(
'id' => $uniq_id,
));
require_celerity_resource('phabricator-jump-nav');
$doc_href = PhabricatorEnv::getDocLink('article/Jump_Nav_User_Guide.html');
$doc_link = phutil_tag(
'a',
array(
'href' => $doc_href,
),
'Jump Nav User Guide');
$jump_input = phutil_tag(
'input',
array(
'type' => 'text',
'class' => 'phabricator-jump-nav',
'name' => 'jump',
'id' => $uniq_id,
'value' => $query,
));
$jump_caption = phutil_tag(
'p',
array(
'class' => 'phabricator-jump-nav-caption',
),
hsprintf(
'Enter the name of an object like <tt>D123</tt> to quickly jump to '.
'it. See %s or type <tt>help</tt>.',
$doc_link));
$form = phabricator_form(
$user,
array(
'action' => '/jump/',
'method' => 'POST',
'class' => 'phabricator-jump-nav-form',
),
array(
$jump_input,
$jump_caption,
));
$panel = new AphrontPanelView();
$panel->setNoBackground();
// $panel->appendChild();
$list_filter = new AphrontListFilterView();
$list_filter->appendChild($form);
$container = phutil_tag('div',
array('class' => 'phabricator-jump-nav-container'),
$list_filter);
return $container;
}
private function renderSectionHeader($title, $href) {
$header = phutil_tag(
'a',
array(
'href' => $href,
),
$title);
return $header;
}
private function renderMiniPanel($title, $body) {
$panel = new AphrontMiniPanelView();
$panel->appendChild(
phutil_tag(
'p',
array(
),
array(
phutil_tag('strong', array(), $title.': '),
$body
)));
$this->minipanels[] = $panel;
}
public function buildAuditPanel() {
$request = $this->getRequest();
$user = $request->getUser();
$phids = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user);
$query = new PhabricatorAuditQuery();
$query->withAuditorPHIDs($phids);
$query->withStatus(PhabricatorAuditQuery::STATUS_OPEN);
$query->withAwaitingUser($user);
$query->needCommitData(true);
$query->setLimit(10);
$audits = $query->execute();
$commits = $query->getCommits();
if (!$audits) {
return $this->renderMinipanel(
'No Audits',
'No commits are waiting for you to audit them.');
}
$view = new PhabricatorAuditListView();
$view->setAudits($audits);
$view->setCommits($commits);
$view->setUser($user);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
$title = pht('Audits');
$href = '/audit/';
$panel = new AphrontPanelView();
$panel->setHeader($this->renderSectionHeader($title, $href));
$panel->appendChild($view);
$panel->setNoBackground();
return $panel;
}
public function buildCommitPanel() {
$request = $this->getRequest();
$user = $request->getUser();
$phids = array($user->getPHID());
$query = new PhabricatorAuditCommitQuery();
$query->withAuthorPHIDs($phids);
$query->withStatus(PhabricatorAuditCommitQuery::STATUS_CONCERN);
$query->needCommitData(true);
$query->setLimit(10);
$commits = $query->execute();
if (!$commits) {
return $this->renderMinipanel(
'No Problem Commits',
'No one has raised concerns with your commits.');
}
$view = new PhabricatorAuditCommitListView();
$view->setCommits($commits);
$view->setUser($user);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
$title = pht('Problem Commits');
$href = '/audit/';
$panel = new AphrontPanelView();
$panel->setHeader($this->renderSectionHeader($title, $href));
$panel->appendChild($view);
$panel->setNoBackground();
return $panel;
}
}
diff --git a/src/applications/maniphest/application/PhabricatorApplicationManiphest.php b/src/applications/maniphest/application/PhabricatorApplicationManiphest.php
index ff3746ebf0..c0b3eaad66 100644
--- a/src/applications/maniphest/application/PhabricatorApplicationManiphest.php
+++ b/src/applications/maniphest/application/PhabricatorApplicationManiphest.php
@@ -1,127 +1,127 @@
<?php
final class PhabricatorApplicationManiphest extends PhabricatorApplication {
public function getShortDescription() {
return 'Tasks and Bugs';
}
public function getBaseURI() {
return '/maniphest/';
}
public function getIconName() {
return 'maniphest';
}
public function getApplicationGroup() {
return self::GROUP_CORE;
}
public function getApplicationOrder() {
return 0.110;
}
public function getFactObjectsForAnalysis() {
return array(
new ManiphestTask(),
);
}
public function getEventListeners() {
return array(
new ManiphestNameIndexEventListener(),
new ManiphestActionMenuEventListener(),
new ManiphestHovercardEventListener(),
);
}
public function getRemarkupRules() {
return array(
new ManiphestRemarkupRule(),
);
}
public function getRoutes() {
return array(
'/T(?P<id>[1-9]\d*)' => 'ManiphestTaskDetailController',
'/maniphest/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' => 'ManiphestTaskListController',
'report/(?:(?P<view>\w+)/)?' => 'ManiphestReportController',
'batch/' => 'ManiphestBatchEditController',
'task/' => array(
'create/' => 'ManiphestTaskEditController',
'edit/(?P<id>[1-9]\d*)/' => 'ManiphestTaskEditController',
'descriptionpreview/' =>
'PhabricatorMarkupPreviewController',
),
'transaction/' => array(
'save/' => 'ManiphestTransactionSaveController',
'preview/(?P<id>[1-9]\d*)/'
=> 'ManiphestTransactionPreviewController',
),
'export/(?P<key>[^/]+)/' => 'ManiphestExportController',
'subpriority/' => 'ManiphestSubpriorityController',
'subscribe/(?P<action>add|rem)/(?P<id>[1-9]\d*)/'
=> 'ManiphestSubscribeController',
),
);
}
public function loadStatus(PhabricatorUser $user) {
$status = array();
$query = id(new ManiphestTaskQuery())
->setViewer($user)
- ->withStatus(ManiphestTaskQuery::STATUS_OPEN)
+ ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants())
->withOwners(array($user->getPHID()));
$count = count($query->execute());
$type = PhabricatorApplicationStatusView::TYPE_WARNING;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText(pht('%s Assigned Task(s)', new PhutilNumber($count)))
->setCount($count);
return $status;
}
public function getQuickCreateItems(PhabricatorUser $viewer) {
$items = array();
$item = id(new PHUIListItemView())
->setName(pht('Maniphest Task'))
->setAppIcon('maniphest-dark')
->setHref($this->getBaseURI().'task/create/');
$items[] = $item;
return $items;
}
protected function getCustomCapabilities() {
return array(
ManiphestCapabilityDefaultView::CAPABILITY => array(
'caption' => pht(
'Default view policy for newly created tasks.'),
),
ManiphestCapabilityDefaultEdit::CAPABILITY => array(
'caption' => pht(
'Default edit policy for newly created tasks.'),
),
ManiphestCapabilityEditStatus::CAPABILITY => array(
),
ManiphestCapabilityEditAssign::CAPABILITY => array(
),
ManiphestCapabilityEditPolicies::CAPABILITY => array(
),
ManiphestCapabilityEditPriority::CAPABILITY => array(
),
ManiphestCapabilityEditProjects::CAPABILITY => array(
),
ManiphestCapabilityBulkEdit::CAPABILITY => array(
),
);
}
}
diff --git a/src/applications/maniphest/conduit/ConduitAPI_maniphest_Method.php b/src/applications/maniphest/conduit/ConduitAPI_maniphest_Method.php
index 2de1faa2db..977d108fbd 100644
--- a/src/applications/maniphest/conduit/ConduitAPI_maniphest_Method.php
+++ b/src/applications/maniphest/conduit/ConduitAPI_maniphest_Method.php
@@ -1,300 +1,300 @@
<?php
/**
* @group conduit
*/
abstract class ConduitAPI_maniphest_Method extends ConduitAPIMethod {
public function getApplication() {
return PhabricatorApplication::getByClass(
'PhabricatorApplicationManiphest');
}
public function defineErrorTypes() {
return array(
'ERR-INVALID-PARAMETER' => 'Missing or malformed parameter.'
);
}
protected function buildTaskInfoDictionary(ManiphestTask $task) {
$results = $this->buildTaskInfoDictionaries(array($task));
return idx($results, $task->getPHID());
}
protected function getTaskFields($is_new) {
$fields = array();
if (!$is_new) {
$fields += array(
'id' => 'optional int',
'phid' => 'optional int',
);
}
$fields += array(
'title' => $is_new ? 'required string' : 'optional string',
'description' => 'optional string',
'ownerPHID' => 'optional phid',
'ccPHIDs' => 'optional list<phid>',
'priority' => 'optional int',
'projectPHIDs' => 'optional list<phid>',
'filePHIDs' => 'optional list<phid>',
'auxiliary' => 'optional dict',
);
if (!$is_new) {
$fields += array(
'status' => 'optional int',
'comments' => 'optional string',
);
}
return $fields;
}
protected function applyRequest(
ManiphestTask $task,
ConduitAPIRequest $request,
$is_new) {
$changes = array();
if ($is_new) {
$task->setTitle((string)$request->getValue('title'));
$task->setDescription((string)$request->getValue('description'));
$changes[ManiphestTransaction::TYPE_STATUS] =
- ManiphestTaskStatus::STATUS_OPEN;
+ ManiphestTaskStatus::getDefaultStatus();
} else {
$comments = $request->getValue('comments');
if (!$is_new && $comments !== null) {
$changes[PhabricatorTransactions::TYPE_COMMENT] = null;
}
$title = $request->getValue('title');
if ($title !== null) {
$changes[ManiphestTransaction::TYPE_TITLE] = $title;
}
$desc = $request->getValue('description');
if ($desc !== null) {
$changes[ManiphestTransaction::TYPE_DESCRIPTION] = $desc;
}
$status = $request->getValue('status');
if ($status !== null) {
$valid_statuses = ManiphestTaskStatus::getTaskStatusMap();
if (!isset($valid_statuses[$status])) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription('Status set to invalid value.');
}
$changes[ManiphestTransaction::TYPE_STATUS] = $status;
}
}
$priority = $request->getValue('priority');
if ($priority !== null) {
$valid_priorities = ManiphestTaskPriority::getTaskPriorityMap();
if (!isset($valid_priorities[$priority])) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription('Priority set to invalid value.');
}
$changes[ManiphestTransaction::TYPE_PRIORITY] = $priority;
}
$owner_phid = $request->getValue('ownerPHID');
if ($owner_phid !== null) {
$this->validatePHIDList(array($owner_phid),
PhabricatorPeoplePHIDTypeUser::TYPECONST,
'ownerPHID');
$changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid;
}
$ccs = $request->getValue('ccPHIDs');
if ($ccs !== null) {
$this->validatePHIDList($ccs,
PhabricatorPeoplePHIDTypeUser::TYPECONST,
'ccPHIDS');
$changes[ManiphestTransaction::TYPE_CCS] = $ccs;
}
$project_phids = $request->getValue('projectPHIDs');
if ($project_phids !== null) {
$this->validatePHIDList($project_phids,
PhabricatorProjectPHIDTypeProject::TYPECONST,
'projectPHIDS');
$changes[ManiphestTransaction::TYPE_PROJECTS] = $project_phids;
}
$file_phids = $request->getValue('filePHIDs');
if ($file_phids !== null) {
$this->validatePHIDList($file_phids,
PhabricatorFilePHIDTypeFile::TYPECONST,
'filePHIDS');
$file_map = array_fill_keys($file_phids, true);
$attached = $task->getAttached();
$attached[PhabricatorFilePHIDTypeFile::TYPECONST] = $file_map;
$changes[ManiphestTransaction::TYPE_ATTACH] = $attached;
}
$template = new ManiphestTransaction();
$transactions = array();
foreach ($changes as $type => $value) {
$transaction = clone $template;
$transaction->setTransactionType($type);
if ($type == PhabricatorTransactions::TYPE_COMMENT) {
$transaction->attachComment(
id(new ManiphestTransactionComment())
->setContent($comments));
} else {
$transaction->setNewValue($value);
}
$transactions[] = $transaction;
}
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_EDIT);
$field_list->readFieldsFromStorage($task);
$auxiliary = $request->getValue('auxiliary');
if ($auxiliary) {
foreach ($field_list->getFields() as $key => $field) {
if (!array_key_exists($key, $auxiliary)) {
continue;
}
$transaction = clone $template;
$transaction->setTransactionType(
PhabricatorTransactions::TYPE_CUSTOMFIELD);
$transaction->setMetadataValue('customfield:key', $key);
$transaction->setOldValue(
$field->getOldValueForApplicationTransactions());
$transaction->setNewValue($auxiliary[$key]);
$transactions[] = $transaction;
}
}
if (!$transactions) {
return;
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($request->getUser());
$event->setConduitRequest($request);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$transactions = $event->getValue('transactions');
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_CONDUIT,
array());
$editor = id(new ManiphestTransactionEditor())
->setActor($request->getUser())
->setContentSource($content_source)
->setContinueOnNoEffect(true);
if (!$is_new) {
$editor->setContinueOnMissingFields(true);
}
$editor->applyTransactions($task, $transactions);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($request->getUser());
$event->setConduitRequest($request);
PhutilEventEngine::dispatchEvent($event);
}
protected function buildTaskInfoDictionaries(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
if (!$tasks) {
return array();
}
$task_phids = mpull($tasks, 'getPHID');
$all_deps = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($task_phids)
->withEdgeTypes(array(PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK));
$all_deps->execute();
$result = array();
foreach ($tasks as $task) {
// TODO: Batch this get as CustomField gets cleaned up.
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_EDIT);
$field_list->readFieldsFromStorage($task);
$auxiliary = mpull(
$field_list->getFields(),
'getValueForStorage',
'getFieldKey');
$task_deps = $all_deps->getDestinationPHIDs(
array($task->getPHID()),
array(PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK));
$result[$task->getPHID()] = array(
'id' => $task->getID(),
'phid' => $task->getPHID(),
'authorPHID' => $task->getAuthorPHID(),
'ownerPHID' => $task->getOwnerPHID(),
'ccPHIDs' => $task->getCCPHIDs(),
'status' => $task->getStatus(),
'priority' => ManiphestTaskPriority::getTaskPriorityName(
$task->getPriority()),
'title' => $task->getTitle(),
'description' => $task->getDescription(),
'projectPHIDs' => $task->getProjectPHIDs(),
'uri' => PhabricatorEnv::getProductionURI('/T'.$task->getID()),
'auxiliary' => $auxiliary,
'objectName' => 'T'.$task->getID(),
'dateCreated' => $task->getDateCreated(),
'dateModified' => $task->getDateModified(),
'dependsOnTaskPHIDs' => $task_deps,
);
}
return $result;
}
/**
* Note this is a temporary stop gap since its easy to make malformed Tasks.
* Long-term, the values set in @{method:defineParamTypes} will be used to
* validate data implicitly within the larger Conduit application.
*
* TODO -- remove this in favor of generalized Conduit hotness
*/
private function validatePHIDList(array $phid_list, $phid_type, $field) {
$phid_groups = phid_group_by_type($phid_list);
unset($phid_groups[$phid_type]);
if (!empty($phid_groups)) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription(
'One or more PHIDs were invalid for '.$field.'.');
}
return true;
}
}
diff --git a/src/applications/maniphest/constants/ManiphestTaskStatus.php b/src/applications/maniphest/constants/ManiphestTaskStatus.php
index e24037c6c3..c6e053efc3 100644
--- a/src/applications/maniphest/constants/ManiphestTaskStatus.php
+++ b/src/applications/maniphest/constants/ManiphestTaskStatus.php
@@ -1,101 +1,160 @@
<?php
/**
* @group maniphest
*/
final class ManiphestTaskStatus extends ManiphestConstants {
const STATUS_OPEN = 0;
const STATUS_CLOSED_RESOLVED = 1;
const STATUS_CLOSED_WONTFIX = 2;
const STATUS_CLOSED_INVALID = 3;
const STATUS_CLOSED_DUPLICATE = 4;
const STATUS_CLOSED_SPITE = 5;
const COLOR_STATUS_OPEN = 'status';
const COLOR_STATUS_CLOSED = 'status-dark';
public static function getTaskStatusMap() {
$open = pht('Open');
$resolved = pht('Resolved');
$wontfix = pht('Wontfix');
$invalid = pht('Invalid');
$duplicate = pht('Duplicate');
$spite = pht('Spite');
return array(
self::STATUS_OPEN => $open,
self::STATUS_CLOSED_RESOLVED => $resolved,
self::STATUS_CLOSED_WONTFIX => $wontfix,
self::STATUS_CLOSED_INVALID => $invalid,
self::STATUS_CLOSED_DUPLICATE => $duplicate,
self::STATUS_CLOSED_SPITE => $spite,
);
}
public static function getTaskStatusFullName($status) {
$open = pht('Open');
$resolved = pht('Closed, Resolved');
$wontfix = pht('Closed, Wontfix');
$invalid = pht('Closed, Invalid');
$duplicate = pht('Closed, Duplicate');
$spite = pht('Closed, Spite');
$map = array(
self::STATUS_OPEN => $open,
self::STATUS_CLOSED_RESOLVED => $resolved,
self::STATUS_CLOSED_WONTFIX => $wontfix,
self::STATUS_CLOSED_INVALID => $invalid,
self::STATUS_CLOSED_DUPLICATE => $duplicate,
self::STATUS_CLOSED_SPITE => $spite,
);
return idx($map, $status, '???');
}
public static function getTaskStatusColor($status) {
$default = self::COLOR_STATUS_OPEN;
$map = array(
self::STATUS_OPEN => self::COLOR_STATUS_OPEN,
self::STATUS_CLOSED_RESOLVED => self::COLOR_STATUS_CLOSED,
self::STATUS_CLOSED_WONTFIX => self::COLOR_STATUS_CLOSED,
self::STATUS_CLOSED_INVALID => self::COLOR_STATUS_CLOSED,
self::STATUS_CLOSED_DUPLICATE => self::COLOR_STATUS_CLOSED,
self::STATUS_CLOSED_SPITE => self::COLOR_STATUS_CLOSED,
);
return idx($map, $status, $default);
}
public static function getIcon($status) {
$default = 'oh-open';
$map = array(
self::STATUS_OPEN => 'oh-open',
self::STATUS_CLOSED_RESOLVED => 'oh-closed-dark',
self::STATUS_CLOSED_WONTFIX => 'oh-closed-dark',
self::STATUS_CLOSED_INVALID => 'oh-closed-dark',
self::STATUS_CLOSED_DUPLICATE => 'oh-closed-dark',
self::STATUS_CLOSED_SPITE => 'oh-closed-dark',
);
return idx($map, $status, $default);
}
public static function renderFullDescription($status) {
$color = self::getTaskStatusColor($status);
$img = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_STATUS)
->setSpriteIcon(self::getIcon($status));
$tag = phutil_tag(
'span',
array(
'class' => 'phui-header-'.$color.' plr',
),
array(
$img,
self::getTaskStatusFullName($status),
));
return $tag;
}
+
+ public static function getDefaultStatus() {
+ return self::STATUS_OPEN;
+ }
+
+ public static function getOpenStatusConstants() {
+ return array(
+ self::STATUS_OPEN,
+ );
+ }
+
+ public static function isOpenStatus($status) {
+ foreach (self::getOpenStatusConstants() as $constant) {
+ if ($status == $constant) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static function getStatusPrefixMap() {
+ return array(
+ 'resolve' => self::STATUS_CLOSED_RESOLVED,
+ 'resolves' => self::STATUS_CLOSED_RESOLVED,
+ 'resolved' => self::STATUS_CLOSED_RESOLVED,
+ 'fix' => self::STATUS_CLOSED_RESOLVED,
+ 'fixes' => self::STATUS_CLOSED_RESOLVED,
+ 'fixed' => self::STATUS_CLOSED_RESOLVED,
+ 'wontfix' => self::STATUS_CLOSED_WONTFIX,
+ 'wontfixes' => self::STATUS_CLOSED_WONTFIX,
+ 'wontfixed' => self::STATUS_CLOSED_WONTFIX,
+ 'spite' => self::STATUS_CLOSED_SPITE,
+ 'spites' => self::STATUS_CLOSED_SPITE,
+ 'spited' => self::STATUS_CLOSED_SPITE,
+ 'invalidate' => self::STATUS_CLOSED_INVALID,
+ 'invaldiates' => self::STATUS_CLOSED_INVALID,
+ 'invalidated' => self::STATUS_CLOSED_INVALID,
+ 'close' => self::STATUS_CLOSED_RESOLVED,
+ 'closes' => self::STATUS_CLOSED_RESOLVED,
+ 'closed' => self::STATUS_CLOSED_RESOLVED,
+ 'ref' => null,
+ 'refs' => null,
+ 'references' => null,
+ 'cf.' => null,
+ );
+ }
+
+ public static function getStatusSuffixMap() {
+ return array(
+ 'as resolved' => self::STATUS_CLOSED_RESOLVED,
+ 'as fixed' => self::STATUS_CLOSED_RESOLVED,
+ 'as wontfix' => self::STATUS_CLOSED_WONTFIX,
+ 'as spite' => self::STATUS_CLOSED_SPITE,
+ 'out of spite' => self::STATUS_CLOSED_SPITE,
+ 'as invalid' => self::STATUS_CLOSED_INVALID,
+ );
+ }
+
+
}
diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php
index 5b7acefd88..eb6fe8ee97 100644
--- a/src/applications/maniphest/controller/ManiphestReportController.php
+++ b/src/applications/maniphest/controller/ManiphestReportController.php
@@ -1,751 +1,751 @@
<?php
/**
* @group maniphest
*/
final class ManiphestReportController extends ManiphestController {
private $view;
public function willProcessRequest(array $data) {
$this->view = idx($data, 'view');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($request->isFormPost()) {
$uri = $request->getRequestURI();
$project = head($request->getArr('set_project'));
$project = nonempty($project, null);
$uri = $uri->alter('project', $project);
$window = $request->getStr('set_window');
$uri = $uri->alter('window', $window);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/maniphest/report/'));
$nav->addLabel(pht('Open Tasks'));
$nav->addFilter('user', pht('By User'));
$nav->addFilter('project', pht('By Project'));
$nav->addLabel(pht('Burnup'));
$nav->addFilter('burn', pht('Burnup Rate'));
$this->view = $nav->selectFilter($this->view, 'user');
require_celerity_resource('maniphest-report-css');
switch ($this->view) {
case 'burn':
$core = $this->renderBurn();
break;
case 'user':
case 'project':
$core = $this->renderOpenTasks();
break;
default:
return new Aphront404Response();
}
$nav->appendChild($core);
$nav->setCrumbs(
$this->buildApplicationCrumbs()
->addTextCrumb(pht('Reports')));
return $this->buildApplicationPage(
$nav,
array(
'title' => pht('Maniphest Reports'),
));
}
public function renderBurn() {
$request = $this->getRequest();
$user = $request->getUser();
$handle = null;
$project_phid = $request->getStr('project');
if ($project_phid) {
$phids = array($project_phid);
$handles = $this->loadViewerHandles($phids);
$handle = $handles[$project_phid];
}
$table = new ManiphestTransaction();
$conn = $table->establishConnection('r');
$joins = '';
if ($project_phid) {
$joins = qsprintf(
$conn,
'JOIN %T t ON x.objectPHID = t.phid
JOIN %T p ON p.taskPHID = t.phid AND p.projectPHID = %s',
id(new ManiphestTask())->getTableName(),
id(new ManiphestTaskProject())->getTableName(),
$project_phid);
}
$data = queryfx_all(
$conn,
'SELECT x.oldValue, x.newValue, x.dateCreated FROM %T x %Q
WHERE transactionType = %s
ORDER BY x.dateCreated ASC',
$table->getTableName(),
$joins,
ManiphestTransaction::TYPE_STATUS);
$stats = array();
$day_buckets = array();
$open_tasks = array();
foreach ($data as $key => $row) {
// NOTE: Hack to avoid json_decode().
$oldv = trim($row['oldValue'], '"');
$newv = trim($row['newValue'], '"');
$old_is_open = ($oldv === (string)ManiphestTaskStatus::STATUS_OPEN);
$new_is_open = ($newv === (string)ManiphestTaskStatus::STATUS_OPEN);
$is_open = ($new_is_open && !$old_is_open);
$is_close = ($old_is_open && !$new_is_open);
$data[$key]['_is_open'] = $is_open;
$data[$key]['_is_close'] = $is_close;
if (!$is_open && !$is_close) {
// This is either some kind of bogus event, or a resolution change
// (e.g., resolved -> invalid). Just skip it.
continue;
}
$day_bucket = phabricator_format_local_time(
$row['dateCreated'],
$user,
'Yz');
$day_buckets[$day_bucket] = $row['dateCreated'];
if (empty($stats[$day_bucket])) {
$stats[$day_bucket] = array(
'open' => 0,
'close' => 0,
);
}
$stats[$day_bucket][$is_close ? 'close' : 'open']++;
}
$template = array(
'open' => 0,
'close' => 0,
);
$rows = array();
$rowc = array();
$last_month = null;
$last_month_epoch = null;
$last_week = null;
$last_week_epoch = null;
$week = null;
$month = null;
$last = last_key($stats) - 1;
$period = $template;
foreach ($stats as $bucket => $info) {
$epoch = $day_buckets[$bucket];
$week_bucket = phabricator_format_local_time(
$epoch,
$user,
'YW');
if ($week_bucket != $last_week) {
if ($week) {
$rows[] = $this->formatBurnRow(
'Week of '.phabricator_date($last_week_epoch, $user),
$week);
$rowc[] = 'week';
}
$week = $template;
$last_week = $week_bucket;
$last_week_epoch = $epoch;
}
$month_bucket = phabricator_format_local_time(
$epoch,
$user,
'Ym');
if ($month_bucket != $last_month) {
if ($month) {
$rows[] = $this->formatBurnRow(
phabricator_format_local_time($last_month_epoch, $user, 'F, Y'),
$month);
$rowc[] = 'month';
}
$month = $template;
$last_month = $month_bucket;
$last_month_epoch = $epoch;
}
$rows[] = $this->formatBurnRow(phabricator_date($epoch, $user), $info);
$rowc[] = null;
$week['open'] += $info['open'];
$week['close'] += $info['close'];
$month['open'] += $info['open'];
$month['close'] += $info['close'];
$period['open'] += $info['open'];
$period['close'] += $info['close'];
}
if ($week) {
$rows[] = $this->formatBurnRow(
pht('Week To Date'),
$week);
$rowc[] = 'week';
}
if ($month) {
$rows[] = $this->formatBurnRow(
pht('Month To Date'),
$month);
$rowc[] = 'month';
}
$rows[] = $this->formatBurnRow(
pht('All Time'),
$period);
$rowc[] = 'aggregate';
$rows = array_reverse($rows);
$rowc = array_reverse($rowc);
$table = new AphrontTableView($rows);
$table->setRowClasses($rowc);
$table->setHeaders(
array(
pht('Period'),
pht('Opened'),
pht('Closed'),
pht('Change'),
));
$table->setColumnClasses(
array(
'right wide',
'n',
'n',
'n',
));
if ($handle) {
$inst = pht(
"NOTE: This table reflects tasks currently in ".
"the project. If a task was opened in the past but added to ".
"the project recently, it is counted on the day it was ".
"opened, not the day it was categorized. If a task was part ".
"of this project in the past but no longer is, it is not ".
"counted at all.");
$header = pht("Task Burn Rate for Project %s", $handle->renderLink());
$caption = phutil_tag('p', array(), $inst);
} else {
$header = pht("Task Burn Rate for All Tasks");
$caption = null;
}
$panel = new AphrontPanelView();
$panel->setHeader($header);
$panel->setCaption($caption);
$panel->appendChild($table);
$tokens = array();
if ($handle) {
$tokens = array($handle);
}
$filter = $this->renderReportFilters($tokens, $has_window = false);
$id = celerity_generate_unique_node_id();
$chart = phutil_tag(
'div',
array(
'id' => $id,
'style' => 'border: 1px solid #6f6f6f; '.
'margin: 1em 2em; '.
'height: 400px; ',
),
'');
list($burn_x, $burn_y) = $this->buildSeries($data);
require_celerity_resource('raphael-core');
require_celerity_resource('raphael-g');
require_celerity_resource('raphael-g-line');
Javelin::initBehavior('line-chart', array(
'hardpoint' => $id,
'x' => array(
$burn_x,
),
'y' => array(
$burn_y,
),
'xformat' => 'epoch',
'yformat' => 'int',
));
return array($filter, $chart, $panel);
}
private function renderReportFilters(array $tokens, $has_window) {
$request = $this->getRequest();
$user = $request->getUser();
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/searchproject/')
->setLabel(pht('Project'))
->setLimit(1)
->setName('set_project')
->setValue($tokens));
if ($has_window) {
list($window_str, $ignored, $window_error) = $this->getWindow();
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Recently Means'))
->setName('set_window')
->setCaption(
pht('Configure the cutoff for the "Recently Closed" column.'))
->setValue($window_str)
->setError($window_error));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Filter By Project')));
$filter = new AphrontListFilterView();
$filter->appendChild($form);
return $filter;
}
private function buildSeries(array $data) {
$out = array();
$counter = 0;
foreach ($data as $row) {
$t = (int)$row['dateCreated'];
if ($row['_is_close']) {
--$counter;
$out[$t] = $counter;
} else if ($row['_is_open']) {
++$counter;
$out[$t] = $counter;
}
}
return array(array_keys($out), array_values($out));
}
private function formatBurnRow($label, $info) {
$delta = $info['open'] - $info['close'];
$fmt = number_format($delta);
if ($delta > 0) {
$fmt = '+'.$fmt;
$fmt = phutil_tag('span', array('class' => 'red'), $fmt);
} else {
$fmt = phutil_tag('span', array('class' => 'green'), $fmt);
}
return array(
$label,
number_format($info['open']),
number_format($info['close']),
$fmt);
}
public function renderOpenTasks() {
$request = $this->getRequest();
$user = $request->getUser();
$query = id(new ManiphestTaskQuery())
->setViewer($user)
- ->withStatus(ManiphestTaskQuery::STATUS_OPEN);
+ ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants());
$project_phid = $request->getStr('project');
$project_handle = null;
if ($project_phid) {
$phids = array($project_phid);
$handles = $this->loadViewerHandles($phids);
$project_handle = $handles[$project_phid];
$query->withAnyProjects($phids);
}
$tasks = $query->execute();
$recently_closed = $this->loadRecentlyClosedTasks();
$date = phabricator_date(time(), $user);
switch ($this->view) {
case 'user':
$result = mgroup($tasks, 'getOwnerPHID');
$leftover = idx($result, '', array());
unset($result['']);
$result_closed = mgroup($recently_closed, 'getOwnerPHID');
$leftover_closed = idx($result_closed, '', array());
unset($result_closed['']);
$base_link = '/maniphest/?assigned=';
$leftover_name = phutil_tag('em', array(), pht('(Up For Grabs)'));
$col_header = pht('User');
$header = pht('Open Tasks by User and Priority (%s)', $date);
break;
case 'project':
$result = array();
$leftover = array();
foreach ($tasks as $task) {
$phids = $task->getProjectPHIDs();
if ($phids) {
foreach ($phids as $project_phid) {
$result[$project_phid][] = $task;
}
} else {
$leftover[] = $task;
}
}
$result_closed = array();
$leftover_closed = array();
foreach ($recently_closed as $task) {
$phids = $task->getProjectPHIDs();
if ($phids) {
foreach ($phids as $project_phid) {
$result_closed[$project_phid][] = $task;
}
} else {
$leftover_closed[] = $task;
}
}
$base_link = '/maniphest/?allProjects[]=';
$leftover_name = phutil_tag('em', array(), pht('(No Project)'));
$col_header = pht('Project');
$header = pht('Open Tasks by Project and Priority (%s)', $date);
break;
}
$phids = array_keys($result);
$handles = $this->loadViewerHandles($phids);
$handles = msort($handles, 'getName');
$order = $request->getStr('order', 'name');
list($order, $reverse) = AphrontTableView::parseSort($order);
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-tooltips', array());
$rows = array();
$pri_total = array();
foreach (array_merge($handles, array(null)) as $handle) {
if ($handle) {
if (($project_handle) &&
($project_handle->getPHID() == $handle->getPHID())) {
// If filtering by, e.g., "bugs", don't show a "bugs" group.
continue;
}
$tasks = idx($result, $handle->getPHID(), array());
$name = phutil_tag(
'a',
array(
'href' => $base_link.$handle->getPHID(),
),
$handle->getName());
$closed = idx($result_closed, $handle->getPHID(), array());
} else {
$tasks = $leftover;
$name = $leftover_name;
$closed = $leftover_closed;
}
$taskv = $tasks;
$tasks = mgroup($tasks, 'getPriority');
$row = array();
$row[] = $name;
$total = 0;
foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $label) {
$n = count(idx($tasks, $pri, array()));
if ($n == 0) {
$row[] = '-';
} else {
$row[] = number_format($n);
}
$total += $n;
}
$row[] = number_format($total);
list($link, $oldest_all) = $this->renderOldest($taskv);
$row[] = $link;
$normal_or_better = array();
foreach ($taskv as $id => $task) {
// TODO: This is sort of a hard-code for the default "normal" status.
// When reports are more powerful, this should be made more general.
if ($task->getPriority() < 50) {
continue;
}
$normal_or_better[$id] = $task;
}
list($link, $oldest_pri) = $this->renderOldest($normal_or_better);
$row[] = $link;
if ($closed) {
$task_ids = implode(',', mpull($closed, 'getID'));
$row[] = phutil_tag(
'a',
array(
'href' => '/maniphest/?ids='.$task_ids,
'target' => '_blank',
),
number_format(count($closed)));
} else {
$row[] = '-';
}
switch ($order) {
case 'total':
$row['sort'] = $total;
break;
case 'oldest-all':
$row['sort'] = $oldest_all;
break;
case 'oldest-pri':
$row['sort'] = $oldest_pri;
break;
case 'closed':
$row['sort'] = count($closed);
break;
case 'name':
default:
$row['sort'] = $handle ? $handle->getName() : '~';
break;
}
$rows[] = $row;
}
$rows = isort($rows, 'sort');
foreach ($rows as $k => $row) {
unset($rows[$k]['sort']);
}
if ($reverse) {
$rows = array_reverse($rows);
}
$cname = array($col_header);
$cclass = array('pri right wide');
$pri_map = ManiphestTaskPriority::getShortNameMap();
foreach ($pri_map as $pri => $label) {
$cname[] = $label;
$cclass[] = 'n';
}
$cname[] = 'Total';
$cclass[] = 'n';
$cname[] = javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht('Oldest open task.'),
'size' => 200,
),
),
pht('Oldest (All)'));
$cclass[] = 'n';
$cname[] = javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht('Oldest open task, excluding those with Low or '.
'Wishlist priority.'),
'size' => 200,
),
),
pht('Oldest (Pri)'));
$cclass[] = 'n';
list($ignored, $window_epoch) = $this->getWindow();
$edate = phabricator_datetime($window_epoch, $user);
$cname[] = javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht('Closed after %s', $edate),
'size' => 260
),
),
pht('Recently Closed'));
$cclass[] = 'n';
$table = new AphrontTableView($rows);
$table->setHeaders($cname);
$table->setColumnClasses($cclass);
$table->makeSortable(
$request->getRequestURI(),
'order',
$order,
$reverse,
array(
'name',
null,
null,
null,
null,
null,
null,
'total',
'oldest-all',
'oldest-pri',
'closed',
));
$panel = new AphrontPanelView();
$panel->setHeader($header);
$panel->appendChild($table);
$tokens = array();
if ($project_handle) {
$tokens = array($project_handle);
}
$filter = $this->renderReportFilters($tokens, $has_window = true);
return array($filter, $panel);
}
/**
* Load all the tasks that have been recently closed.
*/
private function loadRecentlyClosedTasks() {
list($ignored, $window_epoch) = $this->getWindow();
$table = new ManiphestTask();
$xtable = new ManiphestTransaction();
$conn_r = $table->establishConnection('r');
$tasks = queryfx_all(
$conn_r,
'SELECT t.* FROM %T t JOIN %T x ON x.objectPHID = t.phid
WHERE t.status != 0
AND x.oldValue IN (null, %s, %s)
AND x.newValue NOT IN (%s, %s)
AND t.dateModified >= %d
AND x.dateCreated >= %d',
$table->getTableName(),
$xtable->getTableName(),
// TODO: Gross. This table is not meant to be queried like this. Build
// real stats tables.
json_encode((int)ManiphestTaskStatus::STATUS_OPEN),
json_encode((string)ManiphestTaskStatus::STATUS_OPEN),
json_encode((int)ManiphestTaskStatus::STATUS_OPEN),
json_encode((string)ManiphestTaskStatus::STATUS_OPEN),
$window_epoch,
$window_epoch);
return id(new ManiphestTask())->loadAllFromArray($tasks);
}
/**
* Parse the "Recently Means" filter into:
*
* - A string representation, like "12 AM 7 days ago" (default);
* - a locale-aware epoch representation; and
* - a possible error.
*/
private function getWindow() {
$request = $this->getRequest();
$user = $request->getUser();
$window_str = $this->getRequest()->getStr('window', '12 AM 7 days ago');
$error = null;
$window_epoch = null;
// Do locale-aware parsing so that the user's timezone is assumed for
// time windows like "3 PM", rather than assuming the server timezone.
$window_epoch = PhabricatorTime::parseLocalTime($window_str, $user);
if (!$window_epoch) {
$error = 'Invalid';
$window_epoch = time() - (60 * 60 * 24 * 7);
}
// If the time ends up in the future, convert it to the corresponding time
// and equal distance in the past. This is so users can type "6 days" (which
// means "6 days from now") and get the behavior of "6 days ago", rather
// than no results (because the window epoch is in the future). This might
// be a little confusing because it casues "tomorrow" to mean "yesterday"
// and "2022" (or whatever) to mean "ten years ago", but these inputs are
// nonsense anyway.
if ($window_epoch > time()) {
$window_epoch = time() - ($window_epoch - time());
}
return array($window_str, $window_epoch, $error);
}
private function renderOldest(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
$oldest = null;
foreach ($tasks as $id => $task) {
if (($oldest === null) ||
($task->getDateCreated() < $tasks[$oldest]->getDateCreated())) {
$oldest = $id;
}
}
if ($oldest === null) {
return array('-', 0);
}
$oldest = $tasks[$oldest];
$raw_age = (time() - $oldest->getDateCreated());
$age = number_format($raw_age / (24 * 60 * 60)).' d';
$link = javelin_tag(
'a',
array(
'href' => '/T'.$oldest->getID(),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => 'T'.$oldest->getID().': '.$oldest->getTitle(),
),
'target' => '_blank',
),
$age);
return array($link, $raw_age);
}
}
diff --git a/src/applications/maniphest/field/parser/ManiphestCustomFieldStatusParser.php b/src/applications/maniphest/field/parser/ManiphestCustomFieldStatusParser.php
index d9973d6d75..3021433c9f 100644
--- a/src/applications/maniphest/field/parser/ManiphestCustomFieldStatusParser.php
+++ b/src/applications/maniphest/field/parser/ManiphestCustomFieldStatusParser.php
@@ -1,59 +1,29 @@
<?php
final class ManiphestCustomFieldStatusParser
extends PhabricatorCustomFieldMonogramParser {
protected function getPrefixes() {
- return array(
- 'resolve',
- 'resolves',
- 'resolved',
- 'fix',
- 'fixes',
- 'fixed',
- 'wontfix',
- 'wontfixes',
- 'wontfixed',
- 'spite',
- 'spites',
- 'spited',
- 'invalidate',
- 'invalidates',
- 'invalidated',
- 'close',
- 'closes',
- 'closed',
- 'ref',
- 'refs',
- 'references',
- 'cf.',
- );
+ return array_keys(ManiphestTaskStatus::getStatusPrefixMap());
}
protected function getInfixes() {
return array(
'task',
'tasks',
'issue',
'issues',
'bug',
'bugs',
);
}
protected function getSuffixes() {
- return array(
- 'as resolved',
- 'as fixed',
- 'as wontfix',
- 'as spite',
- 'out of spite',
- 'as invalid',
- );
+ return array_keys(ManiphestTaskStatus::getStatusSuffixMap());
}
protected function getMonogramPattern() {
return '[tT]\d+';
}
}
diff --git a/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php b/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php
index be799e6cb4..7d0d5a8ec6 100644
--- a/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php
+++ b/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php
@@ -1,111 +1,110 @@
<?php
final class PhabricatorManiphestTaskTestDataGenerator
extends PhabricatorTestDataGenerator {
public function generate() {
$authorPHID = $this->loadPhabrictorUserPHID();
$author = id(new PhabricatorUser())
->loadOneWhere('phid = %s', $authorPHID);
$task = ManiphestTask::initializeNewTask($author)
->setSubPriority($this->generateTaskSubPriority())
- ->setTitle($this->generateTitle())
- ->setStatus(ManiphestTaskStatus::STATUS_OPEN);
+ ->setTitle($this->generateTitle());
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_UNKNOWN,
array());
$template = new ManiphestTransaction();
// Accumulate Transactions
$changes = array();
$changes[ManiphestTransaction::TYPE_TITLE] =
$this->generateTitle();
$changes[ManiphestTransaction::TYPE_DESCRIPTION] =
$this->generateDescription();
$changes[ManiphestTransaction::TYPE_OWNER] =
$this->loadOwnerPHID();
$changes[ManiphestTransaction::TYPE_STATUS] =
$this->generateTaskStatus();
$changes[ManiphestTransaction::TYPE_PRIORITY] =
$this->generateTaskPriority();
$changes[ManiphestTransaction::TYPE_CCS] =
$this->getCCPHIDs();
$changes[ManiphestTransaction::TYPE_PROJECTS] =
$this->getProjectPHIDs();
$transactions = array();
foreach ($changes as $type => $value) {
$transaction = clone $template;
$transaction->setTransactionType($type);
$transaction->setNewValue($value);
$transactions[] = $transaction;
}
// Apply Transactions
$editor = id(new ManiphestTransactionEditor())
->setActor($author)
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($task, $transactions);
return $task;
}
public function getCCPHIDs() {
$ccs = array();
for ($i = 0; $i < rand(1, 4);$i++) {
$ccs[] = $this->loadPhabrictorUserPHID();
}
return $ccs;
}
public function getProjectPHIDs() {
$projects = array();
for ($i = 0; $i < rand(1, 4);$i++) {
$project = $this->loadOneRandom("PhabricatorProject");
if ($project) {
$projects[] = $project->getPHID();
}
}
return $projects;
}
public function loadOwnerPHID() {
if (rand(0, 3) == 0) {
return null;
} else {
return $this->loadPhabrictorUserPHID();
}
}
public function generateTitle() {
return id(new PhutilLipsumContextFreeGrammar())
->generate();
}
public function generateDescription() {
return id(new PhutilLipsumContextFreeGrammar())
->generateSeveral(rand(30, 40));
}
public function generateTaskPriority() {
return array_rand(ManiphestTaskPriority::getTaskPriorityMap());
}
public function generateTaskSubPriority() {
return rand(2 << 16, 2 << 32);
}
public function generateTaskStatus() {
$statuses = array_keys(ManiphestTaskStatus::getTaskStatusMap());
// Make sure 4/5th of all generated Tasks are open
$random = rand(0, 4);
if ($random != 0) {
- return ManiphestTaskStatus::STATUS_OPEN;
+ return ManiphestTaskStatus::getDefaultStatus();
} else {
return array_rand($statuses);
}
}
}
diff --git a/src/applications/maniphest/mail/ManiphestReplyHandler.php b/src/applications/maniphest/mail/ManiphestReplyHandler.php
index 9f0504655b..e83698c976 100644
--- a/src/applications/maniphest/mail/ManiphestReplyHandler.php
+++ b/src/applications/maniphest/mail/ManiphestReplyHandler.php
@@ -1,189 +1,189 @@
<?php
/**
* @group maniphest
*/
final class ManiphestReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof ManiphestTask)) {
throw new Exception("Mail receiver is not a ManiphestTask!");
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'T');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('T');
}
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.maniphest.reply-handler-domain');
}
public function getReplyHandlerInstructions() {
if ($this->supportsReplies()) {
return "Reply to comment or attach files, or !close, !claim, ".
"!unsubscribe or !assign <username>.";
} else {
return null;
}
}
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
// NOTE: We'll drop in here on both the "reply to a task" and "create a
// new task" workflows! Make sure you test both if you make changes!
$task = $this->getMailReceiver();
$is_new_task = !$task->getID();
$user = $this->getActor();
$body_data = $mail->parseBody();
$body = $body_data['body'];
$body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments());
$xactions = array();
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_EMAIL,
array(
'id' => $mail->getID(),
));
$template = new ManiphestTransaction();
$is_unsub = false;
if ($is_new_task) {
// If this is a new task, create a "User created this task." transaction
// and then set the title and description.
$xaction = clone $template;
$xaction->setTransactionType(ManiphestTransaction::TYPE_STATUS);
- $xaction->setNewValue(ManiphestTaskStatus::STATUS_OPEN);
+ $xaction->setNewValue(ManiphestTaskStatus::getDefaultStatus());
$xactions[] = $xaction;
$task->setAuthorPHID($user->getPHID());
$task->setTitle(nonempty($mail->getSubject(), 'Untitled Task'));
$task->setDescription($body);
$task->setPriority(ManiphestTaskPriority::getDefaultPriority());
} else {
$command = $body_data['command'];
$command_value = $body_data['command_value'];
$ttype = PhabricatorTransactions::TYPE_COMMENT;
$new_value = null;
switch ($command) {
case 'close':
$ttype = ManiphestTransaction::TYPE_STATUS;
$new_value = ManiphestTaskStatus::STATUS_CLOSED_RESOLVED;
break;
case 'claim':
$ttype = ManiphestTransaction::TYPE_OWNER;
$new_value = $user->getPHID();
break;
case 'assign':
$ttype = ManiphestTransaction::TYPE_OWNER;
if ($command_value) {
$assign_users = id(new PhabricatorPeopleQuery())
->setViewer($user)
->withUsernames(array($command_value))
->execute();
if ($assign_users) {
$assign_user = head($assign_users);
$new_value = $assign_user->getPHID();
}
}
// assign to the user by default
if (!$new_value) {
$new_value = $user->getPHID();
}
break;
case 'unsubscribe':
$is_unsub = true;
$ttype = ManiphestTransaction::TYPE_CCS;
$ccs = $task->getCCPHIDs();
foreach ($ccs as $k => $phid) {
if ($phid == $user->getPHID()) {
unset($ccs[$k]);
}
}
$new_value = array_values($ccs);
break;
}
if ($ttype != PhabricatorTransactions::TYPE_COMMENT) {
$xaction = clone $template;
$xaction->setTransactionType($ttype);
$xaction->setNewValue($new_value);
$xactions[] = $xaction;
}
if (strlen($body)) {
$xaction = clone $template;
$xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
$xaction->attachComment(
id(new ManiphestTransactionComment())
->setContent($body));
$xactions[] = $xaction;
}
}
$ccs = $mail->loadCCPHIDs();
$old_ccs = $task->getCCPHIDs();
$new_ccs = array_merge($old_ccs, $ccs);
if (!$is_unsub) {
$new_ccs[] = $user->getPHID();
}
$new_ccs = array_unique($new_ccs);
if (array_diff($new_ccs, $old_ccs)) {
$cc_xaction = clone $template;
$cc_xaction->setTransactionType(ManiphestTransaction::TYPE_CCS);
$cc_xaction->setNewValue($new_ccs);
$xactions[] = $cc_xaction;
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'mail' => $mail,
'new' => $is_new_task,
'transactions' => $xactions,
));
$event->setUser($user);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$xactions = $event->getValue('transactions');
$editor = id(new ManiphestTransactionEditor())
->setActor($user)
->setParentMessageID($mail->getMessageID())
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSource($content_source)
->applyTransactions($task, $xactions);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => $is_new_task,
'transactions' => $xactions,
));
$event->setUser($user);
PhutilEventEngine::dispatchEvent($event);
}
}
diff --git a/src/applications/maniphest/phid/ManiphestPHIDTypeTask.php b/src/applications/maniphest/phid/ManiphestPHIDTypeTask.php
index 2af8135438..99bcd1d8ac 100644
--- a/src/applications/maniphest/phid/ManiphestPHIDTypeTask.php
+++ b/src/applications/maniphest/phid/ManiphestPHIDTypeTask.php
@@ -1,76 +1,76 @@
<?php
final class ManiphestPHIDTypeTask extends PhabricatorPHIDType {
const TYPECONST = 'TASK';
public function getTypeConstant() {
return self::TYPECONST;
}
public function getTypeName() {
return pht('Task');
}
public function newObject() {
return new ManiphestTask();
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new ManiphestTaskQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$task = $objects[$phid];
$id = $task->getID();
$title = $task->getTitle();
$handle->setName("T{$id}");
$handle->setFullName("T{$id}: {$title}");
$handle->setURI("/T{$id}");
- if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) {
+ if (!ManiphestTaskStatus::isOpenStatus($task->getStatus())) {
$handle->setStatus(PhabricatorObjectHandleStatus::STATUS_CLOSED);
}
}
}
public function canLoadNamedObject($name) {
return preg_match('/^T\d*[1-9]\d*$/i', $name);
}
public function loadNamedObjects(
PhabricatorObjectQuery $query,
array $names) {
$id_map = array();
foreach ($names as $name) {
$id = (int)substr($name, 1);
$id_map[$id][] = $name;
}
$objects = id(new ManiphestTaskQuery())
->setViewer($query->getViewer())
->withIDs(array_keys($id_map))
->execute();
$results = array();
foreach ($objects as $id => $object) {
foreach (idx($id_map, $id, array()) as $name) {
$results[$name] = $object;
}
}
return $results;
}
}
diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
index 401bff04b1..df9deff3c0 100644
--- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
+++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
@@ -1,443 +1,449 @@
<?php
final class ManiphestTaskSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getCustomFieldObject() {
return new ManiphestTask();
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
$saved->setParameter(
'assignedPHIDs',
$this->readUsersFromRequest($request, 'assigned'));
$saved->setParameter('withUnassigned', $request->getBool('withUnassigned'));
$saved->setParameter(
'authorPHIDs',
$this->readUsersFromRequest($request, 'authors'));
$saved->setParameter(
'subscriberPHIDs',
$this->readPHIDsFromRequest($request, 'subscribers'));
$saved->setParameter('statuses', $request->getArr('statuses'));
$saved->setParameter('priorities', $request->getArr('priorities'));
$saved->setParameter('group', $request->getStr('group'));
$saved->setParameter('order', $request->getStr('order'));
$ids = $request->getStrList('ids');
foreach ($ids as $key => $id) {
$id = trim($id, ' Tt');
if (!$id || !is_numeric($id)) {
unset($ids[$key]);
} else {
$ids[$key] = $id;
}
}
$saved->setParameter('ids', $ids);
$saved->setParameter('fulltext', $request->getStr('fulltext'));
$saved->setParameter(
'allProjectPHIDs',
$request->getArr('allProjects'));
$saved->setParameter(
'withNoProject',
$request->getBool('withNoProject'));
$saved->setParameter(
'anyProjectPHIDs',
$request->getArr('anyProjects'));
$saved->setParameter(
'excludeProjectPHIDs',
$request->getArr('excludeProjects'));
$saved->setParameter(
'userProjectPHIDs',
$this->readUsersFromRequest($request, 'userProjects'));
$saved->setParameter('createdStart', $request->getStr('createdStart'));
$saved->setParameter('createdEnd', $request->getStr('createdEnd'));
$limit = $request->getInt('limit');
if ($limit > 0) {
$saved->setParameter('limit', $limit);
}
$this->readCustomFieldsFromRequest($request, $saved);
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new ManiphestTaskQuery());
$author_phids = $saved->getParameter('authorPHIDs');
if ($author_phids) {
$query->withAuthors($author_phids);
}
$subscriber_phids = $saved->getParameter('subscriberPHIDs');
if ($subscriber_phids) {
$query->withSubscribers($subscriber_phids);
}
$with_unassigned = $saved->getParameter('withUnassigned');
if ($with_unassigned) {
$query->withOwners(array(null));
} else {
$assigned_phids = $saved->getParameter('assignedPHIDs', array());
if ($assigned_phids) {
$query->withOwners($assigned_phids);
}
}
$statuses = $saved->getParameter('statuses');
if ($statuses) {
$query->withStatuses($statuses);
}
$priorities = $saved->getParameter('priorities');
if ($priorities) {
$query->withPriorities($priorities);
}
$order = $saved->getParameter('order');
$order = idx($this->getOrderValues(), $order);
if ($order) {
$query->setOrderBy($order);
} else {
$query->setOrderBy(head($this->getOrderValues()));
}
$group = $saved->getParameter('group');
$group = idx($this->getGroupValues(), $group);
if ($group) {
$query->setGroupBy($group);
} else {
$query->setGroupBy(head($this->getGroupValues()));
}
$ids = $saved->getParameter('ids');
if ($ids) {
$query->withIDs($ids);
}
$fulltext = $saved->getParameter('fulltext');
if (strlen($fulltext)) {
$query->withFullTextSearch($fulltext);
}
$with_no_project = $saved->getParameter('withNoProject');
if ($with_no_project) {
$query->withAllProjects(array(ManiphestTaskOwner::PROJECT_NO_PROJECT));
} else {
$project_phids = $saved->getParameter('allProjectPHIDs');
if ($project_phids) {
$query->withAllProjects($project_phids);
}
}
$any_project_phids = $saved->getParameter('anyProjectPHIDs');
if ($any_project_phids) {
$query->withAnyProjects($any_project_phids);
}
$exclude_project_phids = $saved->getParameter('excludeProjectPHIDs');
if ($exclude_project_phids) {
$query->withoutProjects($exclude_project_phids);
}
$user_project_phids = $saved->getParameter('userProjectPHIDs');
if ($user_project_phids) {
$query->withAnyUserProjects($user_project_phids);
}
$start = $this->parseDateTime($saved->getParameter('createdStart'));
$end = $this->parseDateTime($saved->getParameter('createdEnd'));
if ($start) {
$query->withDateCreatedAfter($start);
}
if ($end) {
$query->withDateCreatedBefore($end);
}
$this->applyCustomFieldsToQuery($query, $saved);
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$assigned_phids = $saved->getParameter('assignedPHIDs', array());
$author_phids = $saved->getParameter('authorPHIDs', array());
$all_project_phids = $saved->getParameter(
'allProjectPHIDs',
array());
$any_project_phids = $saved->getParameter(
'anyProjectPHIDs',
array());
$exclude_project_phids = $saved->getParameter(
'excludeProjectPHIDs',
array());
$user_project_phids = $saved->getParameter(
'userProjectPHIDs',
array());
$subscriber_phids = $saved->getParameter('subscriberPHIDs', array());
$all_phids = array_merge(
$assigned_phids,
$author_phids,
$all_project_phids,
$any_project_phids,
$exclude_project_phids,
$user_project_phids,
$subscriber_phids);
if ($all_phids) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireViewer())
->withPHIDs($all_phids)
->execute();
} else {
$handles = array();
}
$assigned_handles = array_select_keys($handles, $assigned_phids);
$author_handles = array_select_keys($handles, $author_phids);
$all_project_handles = array_select_keys($handles, $all_project_phids);
$any_project_handles = array_select_keys($handles, $any_project_phids);
$exclude_project_handles = array_select_keys(
$handles,
$exclude_project_phids);
$user_project_handles = array_select_keys($handles, $user_project_phids);
$subscriber_handles = array_select_keys($handles, $subscriber_phids);
$with_unassigned = $saved->getParameter('withUnassigned');
$with_no_projects = $saved->getParameter('withNoProject');
$statuses = $saved->getParameter('statuses', array());
$statuses = array_fuse($statuses);
$status_control = id(new AphrontFormCheckboxControl())
->setLabel(pht('Status'));
foreach (ManiphestTaskStatus::getTaskStatusMap() as $status => $name) {
$status_control->addCheckbox(
'statuses[]',
$status,
$name,
isset($statuses[$status]));
}
$priorities = $saved->getParameter('priorities', array());
$priorities = array_fuse($priorities);
$priority_control = id(new AphrontFormCheckboxControl())
->setLabel(pht('Priority'));
foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $name) {
$priority_control->addCheckbox(
'priorities[]',
$pri,
$name,
isset($priorities[$pri]));
}
$ids = $saved->getParameter('ids', array());
$form
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/accounts/')
->setName('assigned')
->setLabel(pht('Assigned To'))
->setValue($assigned_handles))
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'withUnassigned',
1,
pht('Show only unassigned tasks.'),
$with_unassigned))
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/projects/')
->setName('allProjects')
->setLabel(pht('In All Projects'))
->setValue($all_project_handles))
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'withNoProject',
1,
pht('Show only tasks with no projects.'),
$with_no_projects))
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/projects/')
->setName('anyProjects')
->setLabel(pht('In Any Project'))
->setValue($any_project_handles))
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/projects/')
->setName('excludeProjects')
->setLabel(pht('Not In Projects'))
->setValue($exclude_project_handles))
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/accounts/')
->setName('userProjects')
->setLabel(pht('In Users\' Projects'))
->setValue($user_project_handles))
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/accounts/')
->setName('authors')
->setLabel(pht('Authors'))
->setValue($author_handles))
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/mailable/')
->setName('subscribers')
->setLabel(pht('Subscribers'))
->setValue($subscriber_handles))
->appendChild($status_control)
->appendChild($priority_control)
->appendChild(
id(new AphrontFormSelectControl())
->setName('group')
->setLabel(pht('Group By'))
->setValue($saved->getParameter('group'))
->setOptions($this->getGroupOptions()))
->appendChild(
id(new AphrontFormSelectControl())
->setName('order')
->setLabel(pht('Order By'))
->setValue($saved->getParameter('order'))
->setOptions($this->getOrderOptions()))
->appendChild(
id(new AphrontFormTextControl())
->setName('fulltext')
->setLabel(pht('Contains Text'))
->setValue($saved->getParameter('fulltext')))
->appendChild(
id(new AphrontFormTextControl())
->setName('ids')
->setLabel(pht('Task IDs'))
->setValue(implode(', ', $ids)));
$this->appendCustomFieldsToForm($form, $saved);
$this->buildDateRange(
$form,
$saved,
'createdStart',
pht('Created After'),
'createdEnd',
pht('Created Before'));
$form
->appendChild(
id(new AphrontFormTextControl())
->setName('limit')
->setLabel(pht('Page Size'))
->setValue($saved->getParameter('limit', 100)));
}
protected function getURI($path) {
return '/maniphest/'.$path;
}
public function getBuiltinQueryNames() {
$names = array();
if ($this->requireViewer()->isLoggedIn()) {
$names['assigned'] = pht('Assigned');
$names['authored'] = pht('Authored');
$names['subscribed'] = pht('Subscribed');
}
$names['open'] = pht('Open Tasks');
$names['all'] = pht('All Tasks');
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
$viewer_phid = $this->requireViewer()->getPHID();
switch ($query_key) {
case 'all':
return $query;
case 'assigned':
return $query
->setParameter('assignedPHIDs', array($viewer_phid))
- ->setParameter('statuses', array(ManiphestTaskStatus::STATUS_OPEN));
+ ->setParameter(
+ 'statuses',
+ ManiphestTaskStatus::getOpenStatusConstants());
case 'subscribed':
return $query
->setParameter('subscriberPHIDs', array($viewer_phid))
- ->setParameter('statuses', array(ManiphestTaskStatus::STATUS_OPEN));
+ ->setParameter(
+ 'statuses',
+ ManiphestTaskStatus::getOpenStatusConstants());
case 'open':
return $query
- ->setParameter('statuses', array(ManiphestTaskStatus::STATUS_OPEN));
+ ->setParameter(
+ 'statuses',
+ ManiphestTaskStatus::getOpenStatusConstants());
case 'authored':
return $query
->setParameter('authorPHIDs', array($viewer_phid))
->setParameter('order', 'created')
->setParameter('group', 'none');
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
private function getOrderOptions() {
return array(
'priority' => pht('Priority'),
'updated' => pht('Date Updated'),
'created' => pht('Date Created'),
'title' => pht('Title'),
);
}
private function getOrderValues() {
return array(
'priority' => ManiphestTaskQuery::ORDER_PRIORITY,
'updated' => ManiphestTaskQuery::ORDER_MODIFIED,
'created' => ManiphestTaskQuery::ORDER_CREATED,
'title' => ManiphestTaskQuery::ORDER_TITLE,
);
}
private function getGroupOptions() {
return array(
'priority' => pht('Priority'),
'assigned' => pht('Assigned'),
'status' => pht('Status'),
'project' => pht('Project'),
'none' => pht('None'),
);
}
private function getGroupValues() {
return array(
'priority' => ManiphestTaskQuery::GROUP_PRIORITY,
'assigned' => ManiphestTaskQuery::GROUP_OWNER,
'status' => ManiphestTaskQuery::GROUP_STATUS,
'project' => ManiphestTaskQuery::GROUP_PROJECT,
'none' => ManiphestTaskQuery::GROUP_NONE,
);
}
}
diff --git a/src/applications/maniphest/search/ManiphestSearchIndexer.php b/src/applications/maniphest/search/ManiphestSearchIndexer.php
index 9d67fcf212..971488f64c 100644
--- a/src/applications/maniphest/search/ManiphestSearchIndexer.php
+++ b/src/applications/maniphest/search/ManiphestSearchIndexer.php
@@ -1,88 +1,88 @@
<?php
/**
* @group maniphest
*/
final class ManiphestSearchIndexer
extends PhabricatorSearchDocumentIndexer {
public function getIndexableObject() {
return new ManiphestTask();
}
protected function buildAbstractDocumentByPHID($phid) {
$task = $this->loadDocumentByPHID($phid);
$doc = new PhabricatorSearchAbstractDocument();
$doc->setPHID($task->getPHID());
$doc->setDocumentType(ManiphestPHIDTypeTask::TYPECONST);
$doc->setDocumentTitle($task->getTitle());
$doc->setDocumentCreated($task->getDateCreated());
$doc->setDocumentModified($task->getDateModified());
$doc->addField(
PhabricatorSearchField::FIELD_BODY,
$task->getDescription());
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR,
$task->getAuthorPHID(),
PhabricatorPeoplePHIDTypeUser::TYPECONST,
$task->getDateCreated());
$doc->addRelationship(
- ($task->getStatus() == ManiphestTaskStatus::STATUS_OPEN)
+ (ManiphestTaskStatus::isOpenStatus($task->getStatus()))
? PhabricatorSearchRelationship::RELATIONSHIP_OPEN
: PhabricatorSearchRelationship::RELATIONSHIP_CLOSED,
$task->getPHID(),
ManiphestPHIDTypeTask::TYPECONST,
time());
$this->indexTransactions(
$doc,
new ManiphestTransactionQuery(),
array($phid));
foreach ($task->getProjectPHIDs() as $phid) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_PROJECT,
$phid,
PhabricatorProjectPHIDTypeProject::TYPECONST,
$task->getDateModified()); // Bogus.
}
$owner = $task->getOwnerPHID();
if ($owner) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_OWNER,
$owner,
PhabricatorPeoplePHIDTypeUser::TYPECONST,
time());
} else {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_UNOWNED,
$task->getPHID(),
PhabricatorPHIDConstants::PHID_TYPE_VOID,
$task->getDateCreated());
}
// We need to load handles here since non-users may subscribe (mailing
// lists, e.g.)
$ccs = $task->getCCPHIDs();
$handles = id(new PhabricatorHandleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($ccs)
->execute();
foreach ($ccs as $cc) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER,
$handles[$cc]->getPHID(),
$handles[$cc]->getType(),
time());
}
$this->indexCustomFields($doc, $task);
return $doc;
}
}
diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php
index 20e9620137..055042f1fb 100644
--- a/src/applications/maniphest/storage/ManiphestTask.php
+++ b/src/applications/maniphest/storage/ManiphestTask.php
@@ -1,276 +1,277 @@
<?php
final class ManiphestTask extends ManiphestDAO
implements
PhabricatorMarkupInterface,
PhabricatorPolicyInterface,
PhabricatorTokenReceiverInterface,
PhabricatorFlaggableInterface,
PhrequentTrackableInterface,
PhabricatorCustomFieldInterface {
const MARKUP_FIELD_DESCRIPTION = 'markup:desc';
protected $authorPHID;
protected $ownerPHID;
protected $ccPHIDs = array();
- protected $status = ManiphestTaskStatus::STATUS_OPEN;
+ protected $status;
protected $priority;
protected $subpriority = 0;
protected $title = '';
protected $originalTitle = '';
protected $description = '';
protected $originalEmailSource;
protected $mailKey;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
protected $editPolicy = PhabricatorPolicies::POLICY_USER;
protected $attached = array();
protected $projectPHIDs = array();
private $projectsNeedUpdate;
private $subscribersNeedUpdate;
protected $ownerOrdering;
private $groupByProjectPHID = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
public static function initializeNewTask(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorApplicationManiphest'))
->executeOne();
$view_policy = $app->getPolicy(ManiphestCapabilityDefaultView::CAPABILITY);
$edit_policy = $app->getPolicy(ManiphestCapabilityDefaultEdit::CAPABILITY);
return id(new ManiphestTask())
+ ->setStatus(ManiphestTaskStatus::getDefaultStatus())
->setPriority(ManiphestTaskPriority::getDefaultPriority())
->setAuthorPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy);
}
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 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 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;
}
return $result;
}
/* -( 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) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// The owner of a task can always view and edit it.
$owner_phid = $this->getOwnerPHID();
if ($owner_phid) {
$user_phid = $user->getPHID();
if ($user_phid == $owner_phid) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht(
'The owner of a task can always view and edit it.');
}
/* -( 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(),
)));
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('maniphest.fields');
}
public function getCustomFieldBaseClass() {
return 'ManiphestCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectBoardController.php b/src/applications/project/controller/PhabricatorProjectBoardController.php
index 4afd84eaf8..f7ad05708b 100644
--- a/src/applications/project/controller/PhabricatorProjectBoardController.php
+++ b/src/applications/project/controller/PhabricatorProjectBoardController.php
@@ -1,224 +1,224 @@
<?php
final class PhabricatorProjectBoardController
extends PhabricatorProjectController {
private $id;
private $handles;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->needImages(true)
->withIDs(array($this->id))
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array($project->getPHID()))
->execute();
$columns = mpull($columns, null, 'getSequence');
// If there's no default column, create one now.
if (empty($columns[0])) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$column = PhabricatorProjectColumn::initializeNewColumn($viewer)
->setSequence(0)
->setProjectPHID($project->getPHID())
->save();
$column->attachProject($project);
$columns[0] = $column;
unset($unguarded);
}
ksort($columns);
$tasks = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withAllProjects(array($project->getPHID()))
- ->withStatus(ManiphestTaskQuery::STATUS_OPEN)
+ ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants())
->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY)
->execute();
$tasks = mpull($tasks, null, 'getPHID');
if ($tasks) {
$edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_COLUMN;
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(mpull($tasks, 'getPHID'))
->withEdgeTypes(array($edge_type))
->withDestinationPHIDs(mpull($columns, 'getPHID'));
$edge_query->execute();
}
$task_map = array();
$default_phid = $columns[0]->getPHID();
foreach ($tasks as $task) {
$task_phid = $task->getPHID();
$column_phids = $edge_query->getDestinationPHIDs(array($task_phid));
$column_phid = head($column_phids);
$column_phid = nonempty($column_phid, $default_phid);
$task_map[$column_phid][] = $task_phid;
}
$board_id = celerity_generate_unique_node_id();
$board = id(new PHUIWorkboardView())
->setUser($viewer)
->setFluidishLayout(true)
->setID($board_id);
$this->initBehavior(
'project-boards',
array(
'boardID' => $board_id,
'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'),
));
$this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks);
foreach ($columns as $column) {
$panel = id(new PHUIWorkpanelView())
->setHeader($column->getDisplayName())
->setHeaderColor($column->getHeaderColor())
->setEditURI('edit/'.$column->getID().'/');
$cards = id(new PHUIObjectItemListView())
->setUser($viewer)
->setCards(true)
->setFlush(true)
->setAllowEmptyList(true)
->addSigil('project-column')
->setMetadata(
array(
'columnPHID' => $column->getPHID(),
));
$task_phids = idx($task_map, $column->getPHID(), array());
foreach (array_select_keys($tasks, $task_phids) as $task) {
$cards->addItem($this->renderTaskCard($task));
}
$panel->setCards($cards);
if (!$task_phids) {
$cards->addClass('project-column-empty');
}
$board->addPanel($panel);
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(
$project->getName(),
$this->getApplicationURI('view/'.$project->getID().'/'));
$crumbs->addTextCrumb(pht('Board'));
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
$actions = id(new PhabricatorActionListView())
->setUser($viewer)
->addAction(
id(new PhabricatorActionView())
->setName(pht('Add Column'))
->setHref($this->getApplicationURI('board/'.$this->id.'/edit/'))
->setIcon('create')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$plist = id(new PHUIPropertyListView());
// TODO: Need this to get actions to render.
$plist->addProperty(
pht('Project Boards'),
phutil_tag(
'em',
array(),
pht(
'This feature is beta, but should mostly work.')));
$plist->setActionList($actions);
$header = id(new PHUIHeaderView())
->setHeader($project->getName())
->setUser($viewer)
->setImage($project->getProfileImageURI())
->setPolicyObject($project);
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($plist);
$board_box = id(new PHUIBoxView())
->appendChild($board)
->addMargin(PHUI::MARGIN_LARGE);
return $this->buildApplicationPage(
array(
$crumbs,
$box,
$board_box,
),
array(
'title' => pht('%s Board', $project->getName()),
'device' => true,
));
}
private function renderTaskCard(ManiphestTask $task) {
$request = $this->getRequest();
$viewer = $request->getUser();
$handles = $this->handles;
$color_map = ManiphestTaskPriority::getColorMap();
$bar_color = idx($color_map, $task->getPriority(), 'grey');
// TODO: Batch this earlier on.
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$task,
PhabricatorPolicyCapability::CAN_EDIT);
$card = id(new PHUIObjectItemView())
->setObjectName('T'.$task->getID())
->setHeader($task->getTitle())
->setGrippable($can_edit)
->setHref('/T'.$task->getID())
->addSigil('project-card')
->setMetadata(
array(
'objectPHID' => $task->getPHID(),
))
->addAction(
id(new PHUIListItemView())
->setName(pht('Edit'))
->setIcon('edit')
->setHref('/maniphest/task/edit/'.$task->getID().'/')
->setWorkflow(true))
->setBarColor($bar_color);
if ($task->getOwnerPHID()) {
$owner = $handles[$task->getOwnerPHID()];
$card->addAttribute($owner->renderLink());
}
return $card;
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php
index 6143907f3d..f1d0716384 100644
--- a/src/applications/project/controller/PhabricatorProjectProfileController.php
+++ b/src/applications/project/controller/PhabricatorProjectProfileController.php
@@ -1,265 +1,265 @@
<?php
final class PhabricatorProjectProfileController
extends PhabricatorProjectController {
private $id;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withIDs(array($this->id))
->needMembers(true)
->needImages(true)
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$picture = $project->getProfileImageURI();
require_celerity_resource('phabricator-profile-css');
$tasks = $this->renderTasksPage($project);
$query = new PhabricatorFeedQuery();
$query->setFilterPHIDs(
array(
$project->getPHID(),
));
$query->setLimit(50);
$query->setViewer($this->getRequest()->getUser());
$stories = $query->execute();
$feed = $this->renderStories($stories);
$content = phutil_tag_div(
'phabricator-project-layout',
array($tasks, $feed));
$id = $this->id;
$icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_ICONS)
->setSpriteIcon('workboard');
$board_btn = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Workboards'))
->setHref($this->getApplicationURI("board/{$id}/"))
->setIcon($icon);
$header = id(new PHUIHeaderView())
->setHeader($project->getName())
->setUser($user)
->setPolicyObject($project)
->setImage($picture)
->addActionLink($board_btn);
if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ACTIVE) {
$header->setStatus('oh-ok', '', pht('Active'));
} else {
$header->setStatus('policy-noone', '', pht('Archived'));
}
$actions = $this->buildActionListView($project);
$properties = $this->buildPropertyListView($project, $actions);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($project->getName())
->setActionList($actions);
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
$content,
),
array(
'title' => $project->getName(),
'device' => true,
));
}
private function renderFeedPage(PhabricatorProject $project) {
$query = new PhabricatorFeedQuery();
$query->setFilterPHIDs(array($project->getPHID()));
$query->setViewer($this->getRequest()->getUser());
$query->setLimit(100);
$stories = $query->execute();
if (!$stories) {
return pht('There are no stories about this project.');
}
return $this->renderStories($stories);
}
private function renderStories(array $stories) {
assert_instances_of($stories, 'PhabricatorFeedStory');
$builder = new PhabricatorFeedBuilder($stories);
$builder->setUser($this->getRequest()->getUser());
$builder->setShowHovercards(true);
$view = $builder->buildView();
return phutil_tag_div(
'profile-feed',
$view->render());
}
private function renderTasksPage(PhabricatorProject $project) {
$user = $this->getRequest()->getUser();
$query = id(new ManiphestTaskQuery())
->setViewer($user)
->withAnyProjects(array($project->getPHID()))
- ->withStatus(ManiphestTaskQuery::STATUS_OPEN)
+ ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants())
->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY)
->setLimit(10);
$tasks = $query->execute();
$phids = mpull($tasks, 'getOwnerPHID');
$phids = array_merge(
$phids,
array_mergev(mpull($tasks, 'getProjectPHIDs')));
$phids = array_filter($phids);
$handles = $this->loadViewerHandles($phids);
$task_list = new ManiphestTaskListView();
$task_list->setUser($user);
$task_list->setTasks($tasks);
$task_list->setHandles($handles);
$phid = $project->getPHID();
$view_uri = '/maniphest/?statuses[]=0&allProjects[]='.$phid.'#R';
$create_uri = '/maniphest/task/create/?projects='.$phid;
$icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_ICONS)
->setSpriteIcon('action-menu');
$button_view = id(new PHUIButtonView())
->setTag('a')
->setText(pht('View All'))
->setHref($view_uri)
->setIcon($icon);
$icon_new = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_ICONS)
->setSpriteIcon('new');
$button_add = id(new PHUIButtonView())
->setTag('a')
->setText(pht('New Task'))
->setHref($create_uri)
->setIcon($icon_new);
$header = id(new PHUIHeaderView())
->setHeader(pht('Open Tasks'))
->addActionLink($button_add)
->addActionLink($button_view);
$content = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($task_list);
return $content;
}
private function buildActionListView(PhabricatorProject $project) {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $project->getID();
$view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($project)
->setObjectURI($request->getRequestURI());
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Project'))
->setIcon('edit')
->setHref($this->getApplicationURI("edit/{$id}/")));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Members'))
->setIcon('user')
->setHref($this->getApplicationURI("members/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$action = null;
if (!$project->isUserMember($viewer->getPHID())) {
$can_join = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_JOIN);
$action = id(new PhabricatorActionView())
->setUser($viewer)
->setRenderAsForm(true)
->setHref('/project/update/'.$project->getID().'/join/')
->setIcon('new')
->setDisabled(!$can_join)
->setName(pht('Join Project'));
} else {
$action = id(new PhabricatorActionView())
->setWorkflow(true)
->setHref('/project/update/'.$project->getID().'/leave/')
->setIcon('delete')
->setName(pht('Leave Project...'));
}
$view->addAction($action);
return $view;
}
private function buildPropertyListView(
PhabricatorProject $project,
PhabricatorActionListView $actions) {
$request = $this->getRequest();
$viewer = $request->getUser();
$this->loadHandles($project->getMemberPHIDs());
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($project)
->setActionList($actions);
$view->addProperty(
pht('Members'),
$project->getMemberPHIDs()
? $this->renderHandlesForPHIDs($project->getMemberPHIDs(), ',')
: phutil_tag('em', array(), pht('None')));
$field_list = PhabricatorCustomField::getObjectFields(
$project,
PhabricatorCustomField::ROLE_VIEW);
$field_list->appendFieldsToPropertyList($project, $viewer, $view);
return $view;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Jul 28, 6:12 PM (1 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
187269
Default Alt Text
(121 KB)

Event Timeline