Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index 6c9a7d0089..da0d12db1c 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,1197 +1,1266 @@
<?php
final class ManiphestTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $moreValidationErrors = array();
public function getEditorApplicationClass() {
return 'PhabricatorManiphestApplication';
}
public function getEditorObjectsDescription() {
return pht('Maniphest Tasks');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = ManiphestTransaction::TYPE_PRIORITY;
$types[] = ManiphestTransaction::TYPE_STATUS;
$types[] = ManiphestTransaction::TYPE_TITLE;
$types[] = ManiphestTransaction::TYPE_DESCRIPTION;
$types[] = ManiphestTransaction::TYPE_OWNER;
$types[] = ManiphestTransaction::TYPE_SUBPRIORITY;
$types[] = ManiphestTransaction::TYPE_MERGED_INTO;
$types[] = ManiphestTransaction::TYPE_MERGED_FROM;
$types[] = ManiphestTransaction::TYPE_UNBLOCK;
$types[] = ManiphestTransaction::TYPE_PARENT;
$types[] = ManiphestTransaction::TYPE_COVER_IMAGE;
$types[] = ManiphestTransaction::TYPE_POINTS;
$types[] = PhabricatorTransactions::TYPE_COLUMNS;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
if ($this->getIsNewObject()) {
return null;
}
return (int)$object->getPriority();
case ManiphestTransaction::TYPE_STATUS:
if ($this->getIsNewObject()) {
return null;
}
return $object->getStatus();
case ManiphestTransaction::TYPE_TITLE:
if ($this->getIsNewObject()) {
return null;
}
return $object->getTitle();
case ManiphestTransaction::TYPE_DESCRIPTION:
if ($this->getIsNewObject()) {
return null;
}
return $object->getDescription();
case ManiphestTransaction::TYPE_OWNER:
return nonempty($object->getOwnerPHID(), null);
case ManiphestTransaction::TYPE_SUBPRIORITY:
return $object->getSubpriority();
case ManiphestTransaction::TYPE_COVER_IMAGE:
return $object->getCoverImageFilePHID();
case ManiphestTransaction::TYPE_POINTS:
$points = $object->getPoints();
if ($points !== null) {
$points = (double)$points;
}
return $points;
case ManiphestTransaction::TYPE_MERGED_INTO:
case ManiphestTransaction::TYPE_MERGED_FROM:
return null;
case ManiphestTransaction::TYPE_PARENT:
case PhabricatorTransactions::TYPE_COLUMNS:
return null;
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
return (int)$xaction->getNewValue();
case ManiphestTransaction::TYPE_OWNER:
return nonempty($xaction->getNewValue(), null);
case ManiphestTransaction::TYPE_STATUS:
case ManiphestTransaction::TYPE_TITLE:
case ManiphestTransaction::TYPE_DESCRIPTION:
case ManiphestTransaction::TYPE_SUBPRIORITY:
case ManiphestTransaction::TYPE_MERGED_INTO:
case ManiphestTransaction::TYPE_MERGED_FROM:
case ManiphestTransaction::TYPE_UNBLOCK:
case ManiphestTransaction::TYPE_COVER_IMAGE:
return $xaction->getNewValue();
case ManiphestTransaction::TYPE_PARENT:
case PhabricatorTransactions::TYPE_COLUMNS:
return $xaction->getNewValue();
case ManiphestTransaction::TYPE_POINTS:
$value = $xaction->getNewValue();
if (!strlen($value)) {
$value = null;
}
if ($value !== null) {
$value = (double)$value;
}
return $value;
}
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return (bool)$new;
}
return parent::transactionHasEffect($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
return $object->setPriority($xaction->getNewValue());
case ManiphestTransaction::TYPE_STATUS:
return $object->setStatus($xaction->getNewValue());
case ManiphestTransaction::TYPE_TITLE:
return $object->setTitle($xaction->getNewValue());
case ManiphestTransaction::TYPE_DESCRIPTION:
return $object->setDescription($xaction->getNewValue());
case ManiphestTransaction::TYPE_OWNER:
$phid = $xaction->getNewValue();
// Update the "ownerOrdering" column to contain the full name of the
// owner, if the task is assigned.
$handle = null;
if ($phid) {
$handle = id(new PhabricatorHandleQuery())
->setViewer($this->getActor())
->withPHIDs(array($phid))
->executeOne();
}
if ($handle) {
$object->setOwnerOrdering($handle->getName());
} else {
$object->setOwnerOrdering(null);
}
return $object->setOwnerPHID($phid);
case ManiphestTransaction::TYPE_SUBPRIORITY:
$object->setSubpriority($xaction->getNewValue());
return;
case ManiphestTransaction::TYPE_MERGED_INTO:
$object->setStatus(ManiphestTaskStatus::getDuplicateStatus());
return;
case ManiphestTransaction::TYPE_COVER_IMAGE:
$file_phid = $xaction->getNewValue();
if ($file_phid) {
$file = id(new PhabricatorFileQuery())
->setViewer($this->getActor())
->withPHIDs(array($file_phid))
->executeOne();
} else {
$file = null;
}
if (!$file || !$file->isTransformableImage()) {
$object->setProperty('cover.filePHID', null);
$object->setProperty('cover.thumbnailPHID', null);
return;
}
$xform_key = PhabricatorFileThumbnailTransform::TRANSFORM_WORKCARD;
$xform = PhabricatorFileTransform::getTransformByKey($xform_key)
->executeTransform($file);
$object->setProperty('cover.filePHID', $file->getPHID());
$object->setProperty('cover.thumbnailPHID', $xform->getPHID());
return;
case ManiphestTransaction::TYPE_POINTS:
$object->setPoints($xaction->getNewValue());
return;
case ManiphestTransaction::TYPE_MERGED_FROM:
case ManiphestTransaction::TYPE_PARENT:
case PhabricatorTransactions::TYPE_COLUMNS:
return;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PARENT:
$parent_phid = $xaction->getNewValue();
$parent_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST;
$task_phid = $object->getPHID();
id(new PhabricatorEdgeEditor())
->addEdge($parent_phid, $parent_type, $task_phid)
->save();
break;
case PhabricatorTransactions::TYPE_COLUMNS:
foreach ($xaction->getNewValue() as $move) {
$this->applyBoardMove($object, $move);
}
break;
default:
break;
}
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// When we change the status of a task, update tasks this tasks blocks
// with a message to the effect of "alincoln resolved blocking task Txxx."
$unblock_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_STATUS:
$unblock_xaction = $xaction;
break;
}
}
if ($unblock_xaction !== null) {
$blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);
if ($blocked_phids) {
// In theory we could apply these through policies, but that seems a
// little bit surprising. For now, use the actor's vision.
$blocked_tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withPHIDs($blocked_phids)
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->execute();
$old = $unblock_xaction->getOldValue();
$new = $unblock_xaction->getNewValue();
foreach ($blocked_tasks as $blocked_task) {
$parent_xaction = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_UNBLOCK)
->setOldValue(array($object->getPHID() => $old))
->setNewValue(array($object->getPHID() => $new));
if ($this->getIsNewObject()) {
$parent_xaction->setMetadataValue('blocker.new', true);
}
id(new ManiphestTransactionEditor())
->setActor($this->getActor())
->setActingAsPHID($this->getActingAsPHID())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($blocked_task, array($parent_xaction));
}
}
}
return $xactions;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return 'maniphest-task-'.$object->getPHID();
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
if ($object->getOwnerPHID()) {
$phids[] = $object->getOwnerPHID();
}
$phids[] = $this->getActingAsPHID();
return $phids;
}
public function getMailTagsMap() {
return array(
ManiphestTransaction::MAILTAG_STATUS =>
pht("A task's status changes."),
ManiphestTransaction::MAILTAG_OWNER =>
pht("A task's owner changes."),
ManiphestTransaction::MAILTAG_PRIORITY =>
pht("A task's priority changes."),
ManiphestTransaction::MAILTAG_CC =>
pht("A task's subscribers change."),
ManiphestTransaction::MAILTAG_PROJECTS =>
pht("A task's associated projects change."),
ManiphestTransaction::MAILTAG_UNBLOCK =>
pht('One of the tasks a task is blocked by changes status.'),
ManiphestTransaction::MAILTAG_COLUMN =>
pht('A task is moved between columns on a workboard.'),
ManiphestTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a task.'),
ManiphestTransaction::MAILTAG_OTHER =>
pht('Other task activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ManiphestReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject("T{$id}: {$title}")
->addHeader('Thread-Topic', "T{$id}: ".$object->getOriginalTitle());
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
if ($this->getIsNewObject()) {
$body->addRemarkupSection(
pht('TASK DESCRIPTION'),
$object->getDescription());
}
$body->addLinkSection(
pht('TASK DETAIL'),
PhabricatorEnv::getProductionURI('/T'.$object->getID()));
$board_phids = array();
$type_columns = PhabricatorTransactions::TYPE_COLUMNS;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_columns) {
$moves = $xaction->getNewValue();
foreach ($moves as $move) {
$board_phids[] = $move['boardPHID'];
}
}
}
if ($board_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->requireActor())
->withPHIDs($board_phids)
->execute();
foreach ($projects as $project) {
$body->addLinkSection(
pht('WORKBOARD'),
PhabricatorEnv::getProductionURI(
'/project/board/'.$project->getID().'/'));
}
}
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldSendMail($object, $xactions);
}
protected function supportsSearch() {
return true;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldManiphestTaskAdapter())
->setTask($object);
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
parent::requireCapabilities($object, $xaction);
$app_capability_map = array(
ManiphestTransaction::TYPE_PRIORITY =>
ManiphestEditPriorityCapability::CAPABILITY,
ManiphestTransaction::TYPE_STATUS =>
ManiphestEditStatusCapability::CAPABILITY,
ManiphestTransaction::TYPE_OWNER =>
ManiphestEditAssignCapability::CAPABILITY,
PhabricatorTransactions::TYPE_EDIT_POLICY =>
ManiphestEditPoliciesCapability::CAPABILITY,
PhabricatorTransactions::TYPE_VIEW_POLICY =>
ManiphestEditPoliciesCapability::CAPABILITY,
);
$transaction_type = $xaction->getTransactionType();
$app_capability = null;
if ($transaction_type == PhabricatorTransactions::TYPE_EDGE) {
switch ($xaction->getMetadataValue('edge:type')) {
case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
$app_capability = ManiphestEditProjectsCapability::CAPABILITY;
break;
}
} else {
$app_capability = idx($app_capability_map, $transaction_type);
}
if ($app_capability) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($this->getActor())
->withClasses(array('PhabricatorManiphestApplication'))
->executeOne();
PhabricatorPolicyFilter::requireCapability(
$this->getActor(),
$app,
$app_capability);
}
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_OWNER:
$copy->setOwnerPHID($xaction->getNewValue());
break;
default:
continue;
}
}
return $copy;
}
/**
* Get priorities for moving a task to a new priority.
*/
public static function getEdgeSubpriority(
$priority,
$is_end) {
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPriorities(array($priority))
->setLimit(1);
if ($is_end) {
$query->setOrderVector(array('-priority', '-subpriority', '-id'));
} else {
$query->setOrderVector(array('priority', 'subpriority', 'id'));
}
$result = $query->executeOne();
$step = (double)(2 << 32);
if ($result) {
$base = $result->getSubpriority();
if ($is_end) {
$sub = ($base - $step);
} else {
$sub = ($base + $step);
}
} else {
$sub = 0;
}
return array($priority, $sub);
}
/**
* Get priorities for moving a task before or after another task.
*/
public static function getAdjacentSubpriority(
ManiphestTask $dst,
$is_after,
$allow_recursion = true) {
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->setOrder(ManiphestTaskQuery::ORDER_PRIORITY)
->withPriorities(array($dst->getPriority()))
->setLimit(1);
if ($is_after) {
$query->setAfterID($dst->getID());
} else {
$query->setBeforeID($dst->getID());
}
$adjacent = $query->executeOne();
$base = $dst->getSubpriority();
$step = (double)(2 << 32);
// If we find an adjacent task, we average the two subpriorities and
// return the result.
if ($adjacent) {
$epsilon = 0.01;
// If the adjacent task has a subpriority that is identical or very
// close to the task we're looking at, we're going to move it and all
// tasks with the same subpriority a little farther down the subpriority
// scale.
if ($allow_recursion &&
(abs($adjacent->getSubpriority() - $base) < $epsilon)) {
$conn_w = $adjacent->establishConnection('w');
$min = ($adjacent->getSubpriority() - ($epsilon));
$max = ($adjacent->getSubpriority() + ($epsilon));
// Get all of the tasks with the similar subpriorities to the adjacent
// task, including the adjacent task itself.
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPriorities(array($adjacent->getPriority()))
->withSubpriorityBetween($min, $max);
if (!$is_after) {
$query->setOrderVector(array('-priority', '-subpriority', '-id'));
} else {
$query->setOrderVector(array('priority', 'subpriority', 'id'));
}
$shift_all = $query->execute();
$shift_last = last($shift_all);
// Select the most extreme subpriority in the result set as the
// base value.
$shift_base = head($shift_all)->getSubpriority();
// Find the subpriority before or after the task at the end of the
// block.
list($shift_pri, $shift_sub) = self::getAdjacentSubpriority(
$shift_last,
$is_after,
$allow_recursion = false);
$delta = ($shift_sub - $shift_base);
$count = count($shift_all);
$shift = array();
$cursor = 1;
foreach ($shift_all as $shift_task) {
$shift_target = $shift_base + (($cursor / $count) * $delta);
$cursor++;
queryfx(
$conn_w,
'UPDATE %T SET subpriority = %f WHERE id = %d',
$adjacent->getTableName(),
$shift_target,
$shift_task->getID());
// If we're shifting the adjacent task, update it.
if ($shift_task->getID() == $adjacent->getID()) {
$adjacent->setSubpriority($shift_target);
}
// If we're shifting the original target task, update the base
// subpriority.
if ($shift_task->getID() == $dst->getID()) {
$base = $shift_target;
}
}
}
$sub = ($adjacent->getSubpriority() + $base) / 2;
} else {
// Otherwise, we take a step away from the target's subpriority and
// use that.
if ($is_after) {
$sub = ($base - $step);
} else {
$sub = ($base + $step);
}
}
return array($dst->getPriority(), $sub);
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
switch ($type) {
case ManiphestTransaction::TYPE_TITLE:
$missing = $this->validateIsEmptyTextField(
$object->getTitle(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('Task title is required.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
break;
case ManiphestTransaction::TYPE_PARENT:
$with_effect = array();
foreach ($xactions as $xaction) {
$task_phid = $xaction->getNewValue();
if (!$task_phid) {
continue;
}
$with_effect[] = $xaction;
$task = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withPHIDs(array($task_phid))
->executeOne();
if (!$task) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Parent task identifier "%s" does not identify a visible '.
'task.',
$task_phid),
$xaction);
}
}
if ($with_effect && !$this->getIsNewObject()) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'You can only select a parent task when creating a '.
'transaction for the first time.'),
last($with_effect));
}
break;
case ManiphestTransaction::TYPE_OWNER:
foreach ($xactions as $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
if (!strlen($new)) {
continue;
}
if ($new === $old) {
continue;
}
$assignee_list = id(new PhabricatorPeopleQuery())
->setViewer($this->getActor())
->withPHIDs(array($new))
->execute();
if (!$assignee_list) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'User "%s" is not a valid user.',
$new),
$xaction);
}
}
break;
case ManiphestTransaction::TYPE_COVER_IMAGE:
foreach ($xactions as $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
if (!$new) {
continue;
}
if ($new === $old) {
continue;
}
$file = id(new PhabricatorFileQuery())
->setViewer($this->getActor())
->withPHIDs(array($new))
->executeOne();
if (!$file) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('File "%s" is not valid.', $new),
$xaction);
continue;
}
if (!$file->isTransformableImage()) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('File "%s" is not a valid image file.', $new),
$xaction);
continue;
}
}
break;
case ManiphestTransaction::TYPE_POINTS:
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
if (strlen($new) && !is_numeric($new)) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('Points value must be numeric or empty.'),
$xaction);
continue;
}
if ((double)$new < 0) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('Points value must be nonnegative.'),
$xaction);
continue;
}
}
break;
}
return $errors;
}
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$errors = parent::validateAllTransactions($object, $xactions);
if ($this->moreValidationErrors) {
$errors = array_merge($errors, $this->moreValidationErrors);
}
return $errors;
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$actor = $this->getActor();
$actor_phid = $actor->getPHID();
$results = parent::expandTransactions($object, $xactions);
$is_unassigned = ($object->getOwnerPHID() === null);
$any_assign = false;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == ManiphestTransaction::TYPE_OWNER) {
$any_assign = true;
break;
}
}
$is_open = !$object->isClosed();
$new_status = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_STATUS:
$new_status = $xaction->getNewValue();
break;
}
}
if ($new_status === null) {
$is_closing = false;
} else {
$is_closing = ManiphestTaskStatus::isClosedStatus($new_status);
}
// If the task is not assigned, not being assigned, currently open, and
// being closed, try to assign the actor as the owner.
if ($is_unassigned && !$any_assign && $is_open && $is_closing) {
$is_claim = ManiphestTaskStatus::isClaimStatus($new_status);
// Don't assign the actor if they aren't a real user.
// Don't claim the task if the status is configured to not claim.
if ($actor_phid && $is_claim) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_OWNER)
->setNewValue($actor_phid);
}
}
// Automatically subscribe the author when they create a task.
if ($this->getIsNewObject()) {
if ($actor_phid) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(
array(
'+' => array($actor_phid => $actor_phid),
));
}
}
return $results;
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$results = parent::expandTransaction($object, $xaction);
$type = $xaction->getTransactionType();
switch ($type) {
case PhabricatorTransactions::TYPE_COLUMNS:
try {
- $this->buildMoveTransaction($object, $xaction);
-
- // Implicilty add the task to any boards that we're moving it
- // on, since moves on a board the task isn't part of are not
- // meaningful.
- $board_phids = ipull($xaction->getNewValue(), 'boardPHID');
- if ($board_phids) {
- $results[] = id(new ManiphestTransaction())
- ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
- ->setMetadataValue(
- 'edge:type',
- PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
- ->setIgnoreOnNoEffect(true)
- ->setNewValue(
- array(
- '+' => array_fuse($board_phids),
- ));
+ $more_xactions = $this->buildMoveTransaction($object, $xaction);
+ foreach ($more_xactions as $more_xaction) {
+ $results[] = $more_xaction;
}
} catch (Exception $ex) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$ex->getMessage(),
$xaction);
$this->moreValidationErrors[] = $error;
}
break;
case ManiphestTransaction::TYPE_OWNER:
// If this is a no-op update, don't expand it.
$old_value = $object->getOwnerPHID();
$new_value = $xaction->getNewValue();
if ($old_value === $new_value) {
continue;
}
// When a task is reassigned, move the old owner to the subscriber
// list so they're still in the loop.
if ($old_value) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setIgnoreOnNoEffect(true)
->setNewValue(
array(
'+' => array($old_value => $old_value),
));
}
break;
}
return $results;
}
protected function extractFilePHIDsFromCustomTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$phids = parent::extractFilePHIDsFromCustomTransaction($object, $xaction);
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_COVER_IMAGE:
$phids[] = $xaction->getNewValue();
break;
}
return $phids;
}
private function buildMoveTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$new = $xaction->getNewValue();
if (!is_array($new)) {
$this->validateColumnPHID($new);
$new = array($new);
}
$nearby_phids = array();
foreach ($new as $key => $value) {
if (!is_array($value)) {
$this->validateColumnPHID($value);
$value = array(
'columnPHID' => $value,
);
}
PhutilTypeSpec::checkMap(
$value,
array(
'columnPHID' => 'string',
'beforePHID' => 'optional string',
'afterPHID' => 'optional string',
));
$new[$key] = $value;
if (!empty($value['beforePHID'])) {
$nearby_phids[] = $value['beforePHID'];
}
if (!empty($value['afterPHID'])) {
$nearby_phids[] = $value['afterPHID'];
}
}
if ($nearby_phids) {
$nearby_objects = id(new PhabricatorObjectQuery())
->setViewer($this->getActor())
->withPHIDs($nearby_phids)
->execute();
$nearby_objects = mpull($nearby_objects, null, 'getPHID');
} else {
$nearby_objects = array();
}
$column_phids = ipull($new, 'columnPHID');
if ($column_phids) {
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($this->getActor())
->withPHIDs($column_phids)
->execute();
$columns = mpull($columns, null, 'getPHID');
} else {
$columns = array();
}
$board_phids = mpull($columns, 'getProjectPHID');
$object_phid = $object->getPHID();
$object_phids = $nearby_phids;
// Note that we may not have an object PHID if we're creating a new
// object.
if ($object_phid) {
$object_phids[] = $object_phid;
}
if ($object_phids) {
$layout_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($this->getActor())
->setBoardPHIDs($board_phids)
->setObjectPHIDs($object_phids)
->setFetchAllBoards(true)
->executeLayout();
}
foreach ($new as $key => $spec) {
$column_phid = $spec['columnPHID'];
$column = idx($columns, $column_phid);
if (!$column) {
throw new Exception(
pht(
'Column move transaction specifies column PHID "%s", but there '.
'is no corresponding column with this PHID.',
$column_phid));
}
$board_phid = $column->getProjectPHID();
$nearby = array();
if (!empty($spec['beforePHID'])) {
$nearby['beforePHID'] = $spec['beforePHID'];
}
if (!empty($spec['afterPHID'])) {
$nearby['afterPHID'] = $spec['afterPHID'];
}
if (count($nearby) > 1) {
throw new Exception(
pht(
'Column move transaction moves object to multiple positions. '.
'Specify only "beforePHID" or "afterPHID", not both.'));
}
foreach ($nearby as $where => $nearby_phid) {
if (empty($nearby_objects[$nearby_phid])) {
throw new Exception(
pht(
'Column move transaction specifies object "%s" as "%s", but '.
'there is no corresponding object with this PHID.',
$object_phid,
$where));
}
$nearby_columns = $layout_engine->getObjectColumns(
$board_phid,
$nearby_phid);
$nearby_columns = mpull($nearby_columns, null, 'getPHID');
if (empty($nearby_columns[$column_phid])) {
throw new Exception(
pht(
'Column move transaction specifies object "%s" as "%s" in '.
'column "%s", but this object is not in that column!',
$nearby_phid,
$where,
$column_phid));
}
}
if ($object_phid) {
$old_columns = $layout_engine->getObjectColumns(
$board_phid,
$object_phid);
$old_column_phids = mpull($old_columns, 'getPHID');
} else {
$old_column_phids = array();
}
$spec += array(
'boardPHID' => $board_phid,
'fromColumnPHIDs' => $old_column_phids,
);
// Check if the object is already in this column, and isn't being moved.
// We can just drop this column change if it has no effect.
$from_map = array_fuse($spec['fromColumnPHIDs']);
$already_here = isset($from_map[$column_phid]);
$is_reordering = (bool)$nearby;
if ($already_here && !$is_reordering) {
unset($new[$key]);
} else {
$new[$key] = $spec;
}
}
$new = array_values($new);
$xaction->setNewValue($new);
+
+
+ $more = array();
+
+ // If we're moving the object into a column and it does not already belong
+ // in the column, add the appropriate board. For normal columns, this
+ // is the board PHID. For proxy columns, it is the proxy PHID, unless the
+ // object is already a member of some descendant of the proxy PHID.
+
+ // The major case where this can happen is moves via the API, but it also
+ // happens when a user drags a task from the "Backlog" to a milestone
+ // column.
+
+ if ($object_phid) {
+ $current_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
+ $object_phid,
+ PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
+ $current_phids = array_fuse($current_phids);
+ } else {
+ $current_phids = array();
+ }
+
+ $add_boards = array();
+ foreach ($new as $move) {
+ $column_phid = $move['columnPHID'];
+ $board_phid = $move['boardPHID'];
+ $column = $columns[$column_phid];
+ $proxy_phid = $column->getProxyPHID();
+
+ // If this is a normal column, add the board if the object isn't already
+ // associated.
+ if (!$proxy_phid) {
+ if (!isset($current_phids[$board_phid])) {
+ $add_boards[] = $board_phid;
+ }
+ continue;
+ }
+
+ // If this is a proxy column but the object is already associated with
+ // the proxy board, we don't need to do anything.
+ if (isset($current_phids[$proxy_phid])) {
+ continue;
+ }
+
+ // If this a proxy column and the object is already associated with some
+ // descendant of the proxy board, we also don't need to do anything.
+ $descendants = id(new PhabricatorProjectQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withAncestorProjectPHIDs(array($proxy_phid))
+ ->execute();
+
+ $found_descendant = false;
+ foreach ($descendants as $descendant) {
+ if (isset($current_phids[$descendant->getPHID()])) {
+ $found_descendant = true;
+ break;
+ }
+ }
+
+ if ($found_descendant) {
+ continue;
+ }
+
+ // Otherwise, we're moving the object to a proxy column which it is not
+ // a member of yet, so add an association to the column's proxy board.
+
+ $add_boards[] = $proxy_phid;
+ }
+
+ if ($add_boards) {
+ $more[] = id(new ManiphestTransaction())
+ ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
+ ->setMetadataValue(
+ 'edge:type',
+ PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
+ ->setIgnoreOnNoEffect(true)
+ ->setNewValue(
+ array(
+ '+' => array_fuse($add_boards),
+ ));
+ }
+
+ return $more;
}
private function applyBoardMove($object, array $move) {
$board_phid = $move['boardPHID'];
$column_phid = $move['columnPHID'];
$before_phid = idx($move, 'beforePHID');
$after_phid = idx($move, 'afterPHID');
$object_phid = $object->getPHID();
// We're doing layout with the ominpotent viewer to make sure we don't
// remove positions in columns that exist, but which the actual actor
// can't see.
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
$select_phids = array($board_phid);
$descendants = id(new PhabricatorProjectQuery())
->setViewer($omnipotent_viewer)
->withAncestorProjectPHIDs($select_phids)
->execute();
foreach ($descendants as $descendant) {
$select_phids[] = $descendant->getPHID();
}
$board_tasks = id(new ManiphestTaskQuery())
->setViewer($omnipotent_viewer)
->withEdgeLogicPHIDs(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
array($select_phids))
->execute();
$board_tasks = mpull($board_tasks, null, 'getPHID');
$board_tasks[$object_phid] = $object;
// Make sure tasks are sorted by ID, so we lay out new positions in
// a consistent way.
$board_tasks = msort($board_tasks, 'getID');
$object_phids = array_keys($board_tasks);
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($omnipotent_viewer)
->setBoardPHIDs(array($board_phid))
->setObjectPHIDs($object_phids)
->executeLayout();
// TODO: This logic needs to be revised when we legitimately support
// multiple column positions.
$columns = $engine->getObjectColumns($board_phid, $object_phid);
foreach ($columns as $column) {
$engine->queueRemovePosition(
$board_phid,
$column->getPHID(),
$object_phid);
}
if ($before_phid) {
$engine->queueAddPositionBefore(
$board_phid,
$column_phid,
$object_phid,
$before_phid);
} else if ($after_phid) {
$engine->queueAddPositionAfter(
$board_phid,
$column_phid,
$object_phid,
$after_phid);
} else {
$engine->queueAddPosition(
$board_phid,
$column_phid,
$object_phid);
}
$engine->applyPositionUpdates();
}
private function validateColumnPHID($value) {
if (phid_get_type($value) == PhabricatorProjectColumnPHIDType::TYPECONST) {
return;
}
throw new Exception(
pht(
'When moving objects between columns on a board, columns must '.
'be identified by PHIDs. This transaction uses "%s" to identify '.
'a column, but that is not a valid column PHID.',
$value));
}
}
diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
index 5ead896035..0c3f51ef6b 100644
--- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
+++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
@@ -1,1468 +1,1501 @@
<?php
final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testViewProject() {
$user = $this->createUser();
$user->save();
$user2 = $this->createUser();
$user2->save();
$proj = $this->createProject($user);
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$can_view = PhabricatorPolicyCapability::CAN_VIEW;
// When the view policy is set to "users", any user can see the project.
$this->assertTrue((bool)$this->refreshProject($proj, $user));
$this->assertTrue((bool)$this->refreshProject($proj, $user2));
// When the view policy is set to "no one", members can still see the
// project.
$proj->setViewPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$this->assertTrue((bool)$this->refreshProject($proj, $user));
$this->assertFalse((bool)$this->refreshProject($proj, $user2));
}
public function testIsViewerMemberOrWatcher() {
$user1 = $this->createUser()
->save();
$user2 = $this->createUser()
->save();
$user3 = $this->createUser()
->save();
$proj1 = $this->createProject($user1);
$proj1 = $this->refreshProject($proj1, $user1);
$this->joinProject($proj1, $user1);
$this->joinProject($proj1, $user3);
$this->watchProject($proj1, $user3);
$proj1 = $this->refreshProject($proj1, $user1);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$proj1 = $this->refreshProject($proj1, $user1, false, true);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$this->assertFalse($proj1->isUserWatcher($user1->getPHID()));
$proj1 = $this->refreshProject($proj1, $user1, true, false);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$this->assertFalse($proj1->isUserMember($user2->getPHID()));
$this->assertTrue($proj1->isUserMember($user3->getPHID()));
$proj1 = $this->refreshProject($proj1, $user1, true, true);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$this->assertFalse($proj1->isUserMember($user2->getPHID()));
$this->assertTrue($proj1->isUserMember($user3->getPHID()));
$this->assertFalse($proj1->isUserWatcher($user1->getPHID()));
$this->assertFalse($proj1->isUserWatcher($user2->getPHID()));
$this->assertTrue($proj1->isUserWatcher($user3->getPHID()));
}
public function testEditProject() {
$user = $this->createUser();
$user->save();
$user2 = $this->createUser();
$user2->save();
$proj = $this->createProject($user);
// When edit and view policies are set to "user", anyone can edit.
$proj->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$proj->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$this->assertTrue($this->attemptProjectEdit($proj, $user));
// When edit policy is set to "no one", no one can edit.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$caught = null;
try {
$this->attemptProjectEdit($proj, $user);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
}
public function testAncestorMembers() {
$user1 = $this->createUser();
$user1->save();
$user2 = $this->createUser();
$user2->save();
$parent = $this->createProject($user1);
$child = $this->createProject($user1, $parent);
$this->joinProject($child, $user1);
$this->joinProject($child, $user2);
$project = id(new PhabricatorProjectQuery())
->setViewer($user1)
->withPHIDs(array($child->getPHID()))
->needAncestorMembers(true)
->executeOne();
$members = array_fuse($project->getParentProject()->getMemberPHIDs());
ksort($members);
$expect = array_fuse(
array(
$user1->getPHID(),
$user2->getPHID(),
));
ksort($expect);
$this->assertEqual($expect, $members);
}
public function testAncestryQueries() {
$user = $this->createUser();
$user->save();
$ancestor = $this->createProject($user);
$parent = $this->createProject($user, $ancestor);
$child = $this->createProject($user, $parent);
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(2, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withParentProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(1, count($projects));
$this->assertEqual(
$parent->getPHID(),
head($projects)->getPHID());
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->withDepthBetween(2, null)
->execute();
$this->assertEqual(1, count($projects));
$this->assertEqual(
$child->getPHID(),
head($projects)->getPHID());
$parent2 = $this->createProject($user, $ancestor);
$child2 = $this->createProject($user, $parent2);
$grandchild2 = $this->createProject($user, $child2);
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(5, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withParentProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(2, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->withDepthBetween(2, null)
->execute();
$this->assertEqual(3, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->withDepthBetween(3, null)
->execute();
$this->assertEqual(1, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(
array(
$child->getPHID(),
$grandchild2->getPHID(),
))
->execute();
$this->assertEqual(2, count($projects));
}
public function testMemberMaterialization() {
$material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$child = $this->createProject($user, $parent);
$this->joinProject($child, $user);
$parent_material = PhabricatorEdgeQuery::loadDestinationPHIDs(
$parent->getPHID(),
$material_type);
$this->assertEqual(
array($user->getPHID()),
$parent_material);
}
public function testMilestones() {
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$m1 = $this->createProject($user, $parent, true);
$m2 = $this->createProject($user, $parent, true);
$m3 = $this->createProject($user, $parent, true);
$this->assertEqual(1, $m1->getMilestoneNumber());
$this->assertEqual(2, $m2->getMilestoneNumber());
$this->assertEqual(3, $m3->getMilestoneNumber());
}
public function testMilestoneMembership() {
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$milestone = $this->createProject($user, $parent, true);
$this->joinProject($parent, $user);
$milestone = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($milestone->getPHID()))
->executeOne();
$this->assertTrue($milestone->isUserMember($user->getPHID()));
$milestone = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($milestone->getPHID()))
->needMembers(true)
->executeOne();
$this->assertEqual(
array($user->getPHID()),
$milestone->getMemberPHIDs());
}
public function testSameSlugAsName() {
// It should be OK to type the primary hashtag into "additional hashtags",
// even if the primary hashtag doesn't exist yet because you're creating
// or renaming the project.
$user = $this->createUser();
$user->save();
$project = $this->createProject($user);
// In this first case, set the name and slugs at the same time.
$name = 'slugproject';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME)
->setNewValue($name);
$this->applyTransactions($project, $user, $xactions);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS)
->setNewValue(array($name));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($name, $slugs));
// In this second case, set the name first and then the slugs separately.
$name2 = 'slugproject2';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME)
->setNewValue($name2);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS)
->setNewValue(array($name2));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($name2, $slugs));
}
public function testDuplicateSlugs() {
// Creating a project with multiple duplicate slugs should succeed.
$user = $this->createUser();
$user->save();
$project = $this->createProject($user);
$input = 'duplicate';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS)
->setNewValue(array($input, $input));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($input, $slugs));
}
public function testNormalizeSlugs() {
// When a user creates a project with slug "XxX360n0sc0perXxX", normalize
// it before writing it.
$user = $this->createUser();
$user->save();
$project = $this->createProject($user);
$input = 'NoRmAlIzE';
$expect = 'normalize';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS)
->setNewValue(array($input));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($expect, $slugs));
// If another user tries to add the same slug in denormalized form, it
// should be caught and fail, even though the database version of the slug
// is normalized.
$project2 = $this->createProject($user);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS)
->setNewValue(array($input));
$caught = null;
try {
$this->applyTransactions($project2, $user, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$caught = $ex;
}
$this->assertTrue((bool)$caught);
}
public function testProjectMembersVisibility() {
// This is primarily testing that you can create a project and set the
// visibility or edit policy to "Project Members" immediately.
$user1 = $this->createUser();
$user1->save();
$user2 = $this->createUser();
$user2->save();
$project = PhabricatorProject::initializeNewProject($user1);
$name = pht('Test Project %d', mt_rand());
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME)
->setNewValue($name);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue(
id(new PhabricatorProjectMembersPolicyRule())
->getObjectPolicyFullKey());
$edge_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue(
array(
'=' => array($user1->getPHID() => $user1->getPHID()),
));
$this->applyTransactions($project, $user1, $xactions);
$this->assertTrue((bool)$this->refreshProject($project, $user1));
$this->assertFalse((bool)$this->refreshProject($project, $user2));
$this->leaveProject($project, $user1);
$this->assertFalse((bool)$this->refreshProject($project, $user1));
}
public function testParentProject() {
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$child = $this->createProject($user, $parent);
$this->assertTrue(true);
$child = $this->refreshProject($child, $user);
$this->assertEqual(
$parent->getPHID(),
$child->getParentProject()->getPHID());
$this->assertEqual(1, (int)$child->getProjectDepth());
$this->assertFalse(
$child->isUserMember($user->getPHID()));
$this->assertFalse(
$child->getParentProject()->isUserMember($user->getPHID()));
$this->joinProject($child, $user);
$child = $this->refreshProject($child, $user);
$this->assertTrue(
$child->isUserMember($user->getPHID()));
$this->assertTrue(
$child->getParentProject()->isUserMember($user->getPHID()));
// Test that hiding a parent hides the child.
$user2 = $this->createUser();
$user2->save();
// Second user can see the project for now.
$this->assertTrue((bool)$this->refreshProject($child, $user2));
// Hide the parent.
$this->setViewPolicy($parent, $user, $user->getPHID());
// First user (who can see the parent because they are a member of
// the child) can see the project.
$this->assertTrue((bool)$this->refreshProject($child, $user));
// Second user can not, because they can't see the parent.
$this->assertFalse((bool)$this->refreshProject($child, $user2));
}
public function testSlugMaps() {
// When querying by slugs, slugs should be normalized and the mapping
// should be reported correctly.
$user = $this->createUser();
$user->save();
$name = 'queryslugproject';
$name2 = 'QUERYslugPROJECT';
$slug = 'queryslugextra';
$slug2 = 'QuErYSlUgExTrA';
$project = PhabricatorProject::initializeNewProject($user);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME)
->setNewValue($name);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS)
->setNewValue(array($slug));
$this->applyTransactions($project, $user, $xactions);
$project_query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withSlugs(array($name));
$project_query->execute();
$map = $project_query->getSlugMap();
$this->assertEqual(
array(
$name => $project->getPHID(),
),
ipull($map, 'projectPHID'));
$project_query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withSlugs(array($slug));
$project_query->execute();
$map = $project_query->getSlugMap();
$this->assertEqual(
array(
$slug => $project->getPHID(),
),
ipull($map, 'projectPHID'));
$project_query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withSlugs(array($name, $slug, $name2, $slug2));
$project_query->execute();
$map = $project_query->getSlugMap();
$expect = array(
$name => $project->getPHID(),
$slug => $project->getPHID(),
$name2 => $project->getPHID(),
$slug2 => $project->getPHID(),
);
$actual = ipull($map, 'projectPHID');
ksort($expect);
ksort($actual);
$this->assertEqual($expect, $actual);
$expect = array(
$name => $name,
$slug => $slug,
$name2 => $name,
$slug2 => $slug,
);
$actual = ipull($map, 'slug');
ksort($expect);
ksort($actual);
$this->assertEqual($expect, $actual);
}
public function testJoinLeaveProject() {
$user = $this->createUser();
$user->save();
$proj = $this->createProjectWithNewAuthor();
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
(bool)$proj,
pht(
'Assumption that projects are default visible '.
'to any user when created.'));
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Arbitrary user not member of project.'));
// Join the project.
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Join works.'));
// Join the project again.
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Joining an already-joined project is a no-op.'));
// Leave the project.
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Leave works.'));
// Leave the project again.
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Leaving an already-left project is a no-op.'));
// If a user can't edit or join a project, joining fails.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$caught = null;
try {
$this->joinProject($proj, $user);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($ex instanceof Exception);
// If a user can edit a project, they can join.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Join allowed with edit permission.'));
$this->leaveProject($proj, $user);
// If a user can join a project, they can join, even if they can't edit.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Join allowed with join permission.'));
// A user can leave a project even if they can't edit it or join.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Leave allowed without any permission.'));
}
public function testComplexConstraints() {
$user = $this->createUser();
$user->save();
$engineering = $this->createProject($user);
$engineering_scan = $this->createProject($user, $engineering);
$engineering_warp = $this->createProject($user, $engineering);
$exploration = $this->createProject($user);
$exploration_diplomacy = $this->createProject($user, $exploration);
$task_engineering = $this->newTask(
$user,
array($engineering),
pht('Engineering Only'));
$task_exploration = $this->newTask(
$user,
array($exploration),
pht('Exploration Only'));
$task_warp_explore = $this->newTask(
$user,
array($engineering_warp, $exploration),
pht('Warp to New Planet'));
$task_diplomacy_scan = $this->newTask(
$user,
array($engineering_scan, $exploration_diplomacy),
pht('Scan Diplomat'));
$task_diplomacy = $this->newTask(
$user,
array($exploration_diplomacy),
pht('Diplomatic Meeting'));
$task_warp_scan = $this->newTask(
$user,
array($engineering_scan, $engineering_warp),
pht('Scan Warp Drives'));
$this->assertQueryByProjects(
$user,
array(
$task_engineering,
$task_warp_explore,
$task_diplomacy_scan,
$task_warp_scan,
),
array($engineering),
pht('All Engineering'));
$this->assertQueryByProjects(
$user,
array(
$task_diplomacy_scan,
$task_warp_scan,
),
array($engineering_scan),
pht('All Scan'));
$this->assertQueryByProjects(
$user,
array(
$task_warp_explore,
$task_diplomacy_scan,
),
array($engineering, $exploration),
pht('Engineering + Exploration'));
// This is testing that a query for "Parent" and "Parent > Child" works
// properly.
$this->assertQueryByProjects(
$user,
array(
$task_diplomacy_scan,
$task_warp_scan,
),
array($engineering, $engineering_scan),
pht('Engineering + Scan'));
}
public function testTagAncestryConflicts() {
$user = $this->createUser();
$user->save();
$stonework = $this->createProject($user);
$stonework_masonry = $this->createProject($user, $stonework);
$stonework_sculpting = $this->createProject($user, $stonework);
$task = $this->newTask($user, array());
$this->assertEqual(array(), $this->getTaskProjects($task));
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// Adding a descendant should remove the parent.
$this->addProjectTags($user, $task, array($stonework_masonry->getPHID()));
$this->assertEqual(
array(
$stonework_masonry->getPHID(),
),
$this->getTaskProjects($task));
// Adding an ancestor should remove the descendant.
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// Adding two tags in the same hierarchy which are not mutual ancestors
// should remove the ancestor but otherwise work fine.
$this->addProjectTags(
$user,
$task,
array(
$stonework_masonry->getPHID(),
$stonework_sculpting->getPHID(),
));
$expect = array(
$stonework_masonry->getPHID(),
$stonework_sculpting->getPHID(),
);
sort($expect);
$this->assertEqual($expect, $this->getTaskProjects($task));
}
public function testTagMilestoneConflicts() {
$user = $this->createUser();
$user->save();
$stonework = $this->createProject($user);
$stonework_1 = $this->createProject($user, $stonework, true);
$stonework_2 = $this->createProject($user, $stonework, true);
$task = $this->newTask($user, array());
$this->assertEqual(array(), $this->getTaskProjects($task));
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// Adding a milesone should remove the parent.
$this->addProjectTags($user, $task, array($stonework_1->getPHID()));
$this->assertEqual(
array(
$stonework_1->getPHID(),
),
$this->getTaskProjects($task));
// Adding the parent should remove the milestone.
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// First, add one milestone.
$this->addProjectTags($user, $task, array($stonework_1->getPHID()));
// Now, adding a second milestone should remove the first milestone.
$this->addProjectTags($user, $task, array($stonework_2->getPHID()));
$this->assertEqual(
array(
$stonework_2->getPHID(),
),
$this->getTaskProjects($task));
}
public function testBoardMoves() {
$user = $this->createUser();
$user->save();
$board = $this->createProject($user);
$backlog = $this->addColumn($user, $board, 0);
$column = $this->addColumn($user, $board, 1);
// New tasks should appear in the backlog.
$task1 = $this->newTask($user, array($board));
$expect = array(
$backlog->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task1);
// Moving a task should move it to the destination column.
$this->moveToColumn($user, $board, $task1, $backlog, $column);
$expect = array(
$column->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task1);
// Same thing again, with a new task.
$task2 = $this->newTask($user, array($board));
$expect = array(
$backlog->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task2);
// Move it, too.
$this->moveToColumn($user, $board, $task2, $backlog, $column);
$expect = array(
$column->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task2);
// Now the stuff should be in the column, in order, with the more recently
// moved task on top.
$expect = array(
$task2->getPHID(),
$task1->getPHID(),
);
$this->assertTasksInColumn($expect, $user, $board, $column);
// Move the second task after the first task.
$options = array(
'afterPHID' => $task1->getPHID(),
);
$this->moveToColumn($user, $board, $task2, $column, $column, $options);
$expect = array(
$task1->getPHID(),
$task2->getPHID(),
);
$this->assertTasksInColumn($expect, $user, $board, $column);
// Move the second task before the first task.
$options = array(
'beforePHID' => $task1->getPHID(),
);
$this->moveToColumn($user, $board, $task2, $column, $column, $options);
$expect = array(
$task2->getPHID(),
$task1->getPHID(),
);
$this->assertTasksInColumn($expect, $user, $board, $column);
}
public function testMilestoneMoves() {
$user = $this->createUser();
$user->save();
$board = $this->createProject($user);
$backlog = $this->addColumn($user, $board, 0);
// Create a task into the backlog.
$task = $this->newTask($user, array($board));
$expect = array(
$backlog->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task);
$milestone = $this->createProject($user, $board, true);
$this->addProjectTags($user, $task, array($milestone->getPHID()));
// We just want the side effect of looking at the board: creation of the
// milestone column.
$this->loadColumns($user, $board, $task);
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($user)
->withProjectPHIDs(array($board->getPHID()))
->withProxyPHIDs(array($milestone->getPHID()))
->executeOne();
$this->assertTrue((bool)$column);
// Moving the task to the milestone should have moved it to the milestone
// column.
$expect = array(
$column->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task);
+
+
+ // Move the task within the "Milestone" column. This should not affect
+ // the projects the task is tagged with. See T10912.
+ $task_a = $task;
+
+ $task_b = $this->newTask($user, array($backlog));
+ $this->moveToColumn($user, $board, $task_b, $backlog, $column);
+
+ $a_options = array(
+ 'beforePHID' => $task_b->getPHID(),
+ );
+
+ $b_options = array(
+ 'beforePHID' => $task_a->getPHID(),
+ );
+
+ $old_projects = $this->getTaskProjects($task);
+
+ // Move the target task to the top.
+ $this->moveToColumn($user, $board, $task_a, $column, $column, $a_options);
+ $new_projects = $this->getTaskProjects($task_a);
+ $this->assertEqual($old_projects, $new_projects);
+
+ // Move the other task.
+ $this->moveToColumn($user, $board, $task_b, $column, $column, $b_options);
+ $new_projects = $this->getTaskProjects($task_a);
+ $this->assertEqual($old_projects, $new_projects);
+
+ // Move the target task again.
+ $this->moveToColumn($user, $board, $task_a, $column, $column, $a_options);
+ $new_projects = $this->getTaskProjects($task_a);
+ $this->assertEqual($old_projects, $new_projects);
}
public function testColumnExtendedPolicies() {
$user = $this->createUser();
$user->save();
$board = $this->createProject($user);
$column = $this->addColumn($user, $board, 0);
// At first, the user should be able to view and edit the column.
$column = $this->refreshColumn($user, $column);
$this->assertTrue((bool)$column);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$this->assertTrue($can_edit);
// Now, set the project edit policy to "Members of Project". This should
// disable editing.
$members_policy = id(new PhabricatorProjectMembersPolicyRule())
->getObjectPolicyFullKey();
$board->setEditPolicy($members_policy)->save();
$column = $this->refreshColumn($user, $column);
$this->assertTrue((bool)$column);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$this->assertFalse($can_edit);
// Now, join the project. This should make the column editable again.
$this->joinProject($board, $user);
$column = $this->refreshColumn($user, $column);
$this->assertTrue((bool)$column);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$this->assertTrue($can_edit);
}
private function moveToColumn(
PhabricatorUser $viewer,
PhabricatorProject $board,
ManiphestTask $task,
PhabricatorProjectColumn $src,
PhabricatorProjectColumn $dst,
$options = null) {
$xactions = array();
if (!$options) {
$options = array();
}
$value = array(
'columnPHID' => $dst->getPHID(),
) + $options;
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
->setNewValue(array($value));
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
}
private function assertColumns(
array $expect,
PhabricatorUser $viewer,
PhabricatorProject $board,
ManiphestTask $task) {
$column_phids = $this->loadColumns($viewer, $board, $task);
$this->assertEqual($expect, $column_phids);
}
private function loadColumns(
PhabricatorUser $viewer,
PhabricatorProject $board,
ManiphestTask $task) {
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board->getPHID()))
->setObjectPHIDs(
array(
$task->getPHID(),
))
->executeLayout();
$columns = $engine->getObjectColumns($board->getPHID(), $task->getPHID());
$column_phids = mpull($columns, 'getPHID');
$column_phids = array_values($column_phids);
return $column_phids;
}
private function assertTasksInColumn(
array $expect,
PhabricatorUser $viewer,
PhabricatorProject $board,
PhabricatorProjectColumn $column) {
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board->getPHID()))
->setObjectPHIDs($expect)
->executeLayout();
$object_phids = $engine->getColumnObjectPHIDs(
$board->getPHID(),
$column->getPHID());
$object_phids = array_values($object_phids);
$this->assertEqual($expect, $object_phids);
}
private function addColumn(
PhabricatorUser $viewer,
PhabricatorProject $project,
$sequence) {
$project->setHasWorkboard(1)->save();
return PhabricatorProjectColumn::initializeNewColumn($viewer)
->setSequence(0)
->setProperty('isDefault', ($sequence == 0))
->setProjectPHID($project->getPHID())
->save();
}
private function getTaskProjects(ManiphestTask $task) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$task->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
sort($project_phids);
return $project_phids;
}
private function attemptProjectEdit(
PhabricatorProject $proj,
PhabricatorUser $user,
$skip_refresh = false) {
$proj = $this->refreshProject($proj, $user, true);
$new_name = $proj->getName().' '.mt_rand();
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME);
$xaction->setNewValue($new_name);
$this->applyTransactions($proj, $user, array($xaction));
return true;
}
private function addProjectTags(
PhabricatorUser $viewer,
ManiphestTask $task,
array $phids) {
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setNewValue(
array(
'+' => array_fuse($phids),
));
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
}
private function newTask(
PhabricatorUser $viewer,
array $projects,
$name = null) {
$task = ManiphestTask::initializeNewTask($viewer);
if (!strlen($name)) {
$name = pht('Test Task');
}
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_TITLE)
->setNewValue($name);
if ($projects) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setNewValue(
array(
'=' => array_fuse(mpull($projects, 'getPHID')),
));
}
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
return $task;
}
private function assertQueryByProjects(
PhabricatorUser $viewer,
array $expect,
array $projects,
$label = null) {
$datasource = id(new PhabricatorProjectLogicalDatasource())
->setViewer($viewer);
$project_phids = mpull($projects, 'getPHID');
$constraints = $datasource->evaluateTokens($project_phids);
$query = id(new ManiphestTaskQuery())
->setViewer($viewer);
$query->withEdgeLogicConstraints(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$constraints);
$tasks = $query->execute();
$expect_phids = mpull($expect, 'getTitle', 'getPHID');
ksort($expect_phids);
$actual_phids = mpull($tasks, 'getTitle', 'getPHID');
ksort($actual_phids);
$this->assertEqual($expect_phids, $actual_phids, $label);
}
private function refreshProject(
PhabricatorProject $project,
PhabricatorUser $viewer,
$need_members = false,
$need_watchers = false) {
$results = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->needMembers($need_members)
->needWatchers($need_watchers)
->withIDs(array($project->getID()))
->execute();
if ($results) {
return head($results);
} else {
return null;
}
}
private function refreshColumn(
PhabricatorUser $viewer,
PhabricatorProjectColumn $column) {
$results = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withIDs(array($column->getID()))
->execute();
if ($results) {
return head($results);
} else {
return null;
}
}
private function createProject(
PhabricatorUser $user,
PhabricatorProject $parent = null,
$is_milestone = false) {
$project = PhabricatorProject::initializeNewProject($user);
$name = pht('Test Project %d', mt_rand());
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME)
->setNewValue($name);
if ($parent) {
if ($is_milestone) {
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_MILESTONE)
->setNewValue($parent->getPHID());
} else {
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_PARENT)
->setNewValue($parent->getPHID());
}
}
$this->applyTransactions($project, $user, $xactions);
// Force these values immediately; they are normally updated by the
// index engine.
if ($parent) {
if ($is_milestone) {
$parent->setHasMilestones(1)->save();
} else {
$parent->setHasSubprojects(1)->save();
}
}
return $project;
}
private function setViewPolicy(
PhabricatorProject $project,
PhabricatorUser $user,
$policy) {
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($policy);
$this->applyTransactions($project, $user, $xactions);
return $project;
}
private function createProjectWithNewAuthor() {
$author = $this->createUser();
$author->save();
$project = $this->createProject($author);
return $project;
}
private function createUser() {
$rand = mt_rand();
$user = new PhabricatorUser();
$user->setUsername('unittestuser'.$rand);
$user->setRealName(pht('Unit Test User %d', $rand));
return $user;
}
private function joinProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->joinOrLeaveProject($project, $user, '+');
}
private function leaveProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->joinOrLeaveProject($project, $user, '-');
}
private function watchProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->watchOrUnwatchProject($project, $user, '+');
}
private function unwatchProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->watchOrUnwatchProject($project, $user, '-');
}
private function joinOrLeaveProject(
PhabricatorProject $project,
PhabricatorUser $user,
$operation) {
return $this->applyProjectEdgeTransaction(
$project,
$user,
$operation,
PhabricatorProjectProjectHasMemberEdgeType::EDGECONST);
}
private function watchOrUnwatchProject(
PhabricatorProject $project,
PhabricatorUser $user,
$operation) {
return $this->applyProjectEdgeTransaction(
$project,
$user,
$operation,
PhabricatorObjectHasWatcherEdgeType::EDGECONST);
}
private function applyProjectEdgeTransaction(
PhabricatorProject $project,
PhabricatorUser $user,
$operation,
$edge_type) {
$spec = array(
$operation => array($user->getPHID() => $user->getPHID()),
);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue($spec);
$this->applyTransactions($project, $user, $xactions);
return $project;
}
private function applyTransactions(
PhabricatorProject $project,
PhabricatorUser $user,
array $xactions) {
$editor = id(new PhabricatorProjectTransactionEditor())
->setActor($user)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($project, $xactions);
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php
index e529fab61e..c92c843204 100644
--- a/src/applications/project/controller/PhabricatorProjectMoveController.php
+++ b/src/applications/project/controller/PhabricatorProjectMoveController.php
@@ -1,229 +1,203 @@
<?php
final class PhabricatorProjectMoveController
extends PhabricatorProjectController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$request->validateCSRF();
$column_phid = $request->getStr('columnPHID');
$object_phid = $request->getStr('objectPHID');
$after_phid = $request->getStr('afterPHID');
$before_phid = $request->getStr('beforePHID');
$order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER);
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
))
->withIDs(array($id))
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$board_phid = $project->getPHID();
$object = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withPHIDs(array($object_phid))
->needProjectPHIDs(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
return new Aphront404Response();
}
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array($project->getPHID()))
->execute();
$columns = mpull($columns, null, 'getPHID');
$column = idx($columns, $column_phid);
if (!$column) {
// User is trying to drop this object into a nonexistent column, just kick
// them out.
return new Aphront404Response();
}
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board_phid))
->setObjectPHIDs(array($object_phid))
->executeLayout();
$columns = $engine->getObjectColumns($board_phid, $object_phid);
$old_column_phids = mpull($columns, 'getPHID');
$xactions = array();
$order_params = array();
if ($order == PhabricatorProjectColumn::ORDER_NATURAL) {
if ($after_phid) {
$order_params['afterPHID'] = $after_phid;
} else if ($before_phid) {
$order_params['beforePHID'] = $before_phid;
}
}
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
->setNewValue(
array(
array(
'columnPHID' => $column->getPHID(),
) + $order_params,
));
if ($order == PhabricatorProjectColumn::ORDER_PRIORITY) {
$priority_xactions = $this->getPriorityTransactions(
$object,
$after_phid,
$before_phid);
foreach ($priority_xactions as $xaction) {
$xactions[] = $xaction;
}
}
- $proxy = $column->getProxy();
- if ($proxy) {
- // We're moving the task into a subproject or milestone column, so add
- // the subproject or milestone.
- $add_projects = array($proxy->getPHID());
- } else if ($project->getHasSubprojects() || $project->getHasMilestones()) {
- // We're moving the task into the "Backlog" column on the parent project,
- // so add the parent explicitly. This gets rid of any subproject or
- // milestone tags.
- $add_projects = array($project->getPHID());
- } else {
- $add_projects = array();
- }
-
- if ($add_projects) {
- $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
-
- $xactions[] = id(new ManiphestTransaction())
- ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
- ->setMetadataValue('edge:type', $project_type)
- ->setNewValue(
- array(
- '+' => array_fuse($add_projects),
- ));
- }
-
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request);
$editor->applyTransactions($object, $xactions);
return $this->newCardResponse($board_phid, $object_phid);
}
private function getPriorityTransactions(
ManiphestTask $task,
$after_phid,
$before_phid) {
list($after_task, $before_task) = $this->loadPriorityTasks(
$after_phid,
$before_phid);
$must_move = false;
if ($after_task && !$task->isLowerPriorityThan($after_task)) {
$must_move = true;
}
if ($before_task && !$task->isHigherPriorityThan($before_task)) {
$must_move = true;
}
// The move doesn't require a priority change to be valid, so don't
// change the priority since we are not being forced to.
if (!$must_move) {
return array();
}
$try = array(
array($after_task, true),
array($before_task, false),
);
$pri = null;
$sub = null;
foreach ($try as $spec) {
list($task, $is_after) = $spec;
if (!$task) {
continue;
}
list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority(
$task,
$is_after);
}
$xactions = array();
if ($pri !== null) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_PRIORITY)
->setNewValue($pri);
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY)
->setNewValue($sub);
}
return $xactions;
}
private function loadPriorityTasks($after_phid, $before_phid) {
$viewer = $this->getViewer();
$task_phids = array();
if ($after_phid) {
$task_phids[] = $after_phid;
}
if ($before_phid) {
$task_phids[] = $before_phid;
}
if (!$task_phids) {
return array(null, null);
}
$tasks = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withPHIDs($task_phids)
->execute();
$tasks = mpull($tasks, null, 'getPHID');
if ($after_phid) {
$after_task = idx($tasks, $after_phid);
} else {
$after_task = null;
}
if ($before_phid) {
$before_task = idx($tasks, $before_phid);
} else {
$before_task = null;
}
return array($after_task, $before_task);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Mar 17, 1:15 AM (15 h, 9 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
964020
Default Alt Text
(90 KB)

Event Timeline