Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2890476
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
80 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
index 36c78386f3..3bd38353f6 100644
--- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
+++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
@@ -1,1033 +1,1066 @@
<?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);
}
- 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;
- }
-
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'));
}
+ 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 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(PhabricatorContentSource::newConsoleSource())
->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 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);
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(PhabricatorContentSource::newConsoleSource())
->setContinueOnNoEffect(true)
->applyTransactions($project, $xactions);
}
}
diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
index 26cc3af86e..6db9b774e6 100644
--- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
+++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
@@ -1,920 +1,920 @@
<?php
final class PhabricatorProjectTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $isMilestone;
private function setIsMilestone($is_milestone) {
$this->isMilestone = $is_milestone;
return $this;
}
private function getIsMilestone() {
return $this->isMilestone;
}
public function getEditorApplicationClass() {
return 'PhabricatorProjectApplication';
}
public function getEditorObjectsDescription() {
return pht('Projects');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PhabricatorTransactions::TYPE_JOIN_POLICY;
$types[] = PhabricatorProjectTransaction::TYPE_NAME;
$types[] = PhabricatorProjectTransaction::TYPE_SLUGS;
$types[] = PhabricatorProjectTransaction::TYPE_STATUS;
$types[] = PhabricatorProjectTransaction::TYPE_IMAGE;
$types[] = PhabricatorProjectTransaction::TYPE_ICON;
$types[] = PhabricatorProjectTransaction::TYPE_COLOR;
$types[] = PhabricatorProjectTransaction::TYPE_LOCKED;
$types[] = PhabricatorProjectTransaction::TYPE_PARENT;
$types[] = PhabricatorProjectTransaction::TYPE_MILESTONE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_NAME:
return $object->getName();
case PhabricatorProjectTransaction::TYPE_SLUGS:
$slugs = $object->getSlugs();
$slugs = mpull($slugs, 'getSlug', 'getSlug');
unset($slugs[$object->getPrimarySlug()]);
return array_keys($slugs);
case PhabricatorProjectTransaction::TYPE_STATUS:
return $object->getStatus();
case PhabricatorProjectTransaction::TYPE_IMAGE:
return $object->getProfileImagePHID();
case PhabricatorProjectTransaction::TYPE_ICON:
return $object->getIcon();
case PhabricatorProjectTransaction::TYPE_COLOR:
return $object->getColor();
case PhabricatorProjectTransaction::TYPE_LOCKED:
return (int)$object->getIsMembershipLocked();
case PhabricatorProjectTransaction::TYPE_PARENT:
case PhabricatorProjectTransaction::TYPE_MILESTONE:
return null;
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_NAME:
case PhabricatorProjectTransaction::TYPE_STATUS:
case PhabricatorProjectTransaction::TYPE_IMAGE:
case PhabricatorProjectTransaction::TYPE_ICON:
case PhabricatorProjectTransaction::TYPE_COLOR:
case PhabricatorProjectTransaction::TYPE_LOCKED:
case PhabricatorProjectTransaction::TYPE_PARENT:
case PhabricatorProjectTransaction::TYPE_MILESTONE:
return $xaction->getNewValue();
case PhabricatorProjectTransaction::TYPE_SLUGS:
return $this->normalizeSlugs($xaction->getNewValue());
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_NAME:
$name = $xaction->getNewValue();
$object->setName($name);
if (!$this->getIsMilestone()) {
$object->setPrimarySlug(PhabricatorSlug::normalizeProjectSlug($name));
}
return;
case PhabricatorProjectTransaction::TYPE_SLUGS:
return;
case PhabricatorProjectTransaction::TYPE_STATUS:
$object->setStatus($xaction->getNewValue());
return;
case PhabricatorProjectTransaction::TYPE_IMAGE:
$object->setProfileImagePHID($xaction->getNewValue());
return;
case PhabricatorProjectTransaction::TYPE_ICON:
$object->setIcon($xaction->getNewValue());
return;
case PhabricatorProjectTransaction::TYPE_COLOR:
$object->setColor($xaction->getNewValue());
return;
case PhabricatorProjectTransaction::TYPE_LOCKED:
$object->setIsMembershipLocked($xaction->getNewValue());
return;
case PhabricatorProjectTransaction::TYPE_PARENT:
$object->setParentProjectPHID($xaction->getNewValue());
return;
case PhabricatorProjectTransaction::TYPE_MILESTONE:
$number = $object->getParentProject()->loadNextMilestoneNumber();
$object->setMilestoneNumber($number);
$object->setParentProjectPHID($xaction->getNewValue());
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_NAME:
// First, add the old name as a secondary slug; this is helpful
// for renames and generally a good thing to do.
if ($old !== null) {
$this->addSlug($object, $old, false);
}
$this->addSlug($object, $new, false);
return;
case PhabricatorProjectTransaction::TYPE_SLUGS:
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
foreach ($add as $slug) {
$this->addSlug($object, $slug, true);
}
$this->removeSlugs($object, $rem);
return;
case PhabricatorProjectTransaction::TYPE_STATUS:
case PhabricatorProjectTransaction::TYPE_IMAGE:
case PhabricatorProjectTransaction::TYPE_ICON:
case PhabricatorProjectTransaction::TYPE_COLOR:
case PhabricatorProjectTransaction::TYPE_LOCKED:
case PhabricatorProjectTransaction::TYPE_PARENT:
case PhabricatorProjectTransaction::TYPE_MILESTONE:
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function applyBuiltinExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $xaction->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST:
case PhabricatorObjectHasWatcherEdgeType::EDGECONST:
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
// When adding members or watchers, we add subscriptions.
$add = array_keys(array_diff_key($new, $old));
// When removing members, we remove their subscription too.
// When unwatching, we leave subscriptions, since it's fine to be
// subscribed to a project but not be a member of it.
$edge_const = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
if ($edge_type == $edge_const) {
$rem = array_keys(array_diff_key($old, $new));
} else {
$rem = array();
}
// NOTE: The subscribe is "explicit" because there's no implicit
// unsubscribe, so Join -> Leave -> Join doesn't resubscribe you
// if we use an implicit subscribe, even though you never willfully
// unsubscribed. Not sure if adding implicit unsubscribe (which
// would not write the unsubscribe row) is justified to deal with
// this, which is a fairly weird edge case and pretty arguable both
// ways.
// Subscriptions caused by watches should also clearly be explicit,
// and that case is unambiguous.
id(new PhabricatorSubscriptionsEditor())
->setActor($this->requireActor())
->setObject($object)
->subscribeExplicit($add)
->unsubscribe($rem)
->save();
if ($rem) {
// When removing members, also remove any watches on the project.
$edge_editor = new PhabricatorEdgeEditor();
foreach ($rem as $rem_phid) {
$edge_editor->removeEdge(
$object->getPHID(),
PhabricatorObjectHasWatcherEdgeType::EDGECONST,
$rem_phid);
}
$edge_editor->save();
}
break;
}
break;
}
return parent::applyBuiltinExternalTransaction($object, $xaction);
}
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$errors = array();
// Prevent creating projects which are both subprojects and milestones,
// since this does not make sense, won't work, and will break everything.
$parent_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_PARENT:
case PhabricatorProjectTransaction::TYPE_MILESTONE:
if ($xaction->getNewValue() === null) {
continue;
}
if (!$parent_xaction) {
$parent_xaction = $xaction;
continue;
}
$errors[] = new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
pht('Invalid'),
pht(
'When creating a project, specify a maximum of one parent '.
'project or milestone project. A project can not be both a '.
'subproject and a milestone.'),
$xaction);
break;
break;
}
}
$is_milestone = $this->getIsMilestone();
$is_parent = $object->getHasSubprojects();
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_MEMBERS:
if ($is_parent) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
pht('Invalid'),
pht(
'You can not change members of a project with subprojects '.
'directly. Members of any subproject are automatically '.
'members of the parent project.'),
$xaction);
}
if ($is_milestone) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
pht('Invalid'),
pht(
'You can not change members of a milestone. Members of the '.
'parent project are automatically members of the milestone.'),
$xaction);
}
break;
}
}
return $errors;
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
switch ($type) {
case PhabricatorProjectTransaction::TYPE_NAME:
$missing = $this->validateIsEmptyTextField(
$object->getName(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('Project name is required.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
if (!$xactions) {
break;
}
if ($this->getIsMilestone()) {
break;
}
$name = last($xactions)->getNewValue();
if (!PhabricatorSlug::isValidProjectSlug($name)) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Project names must contain at least one letter or number.'),
last($xactions));
break;
}
$slug = PhabricatorSlug::normalizeProjectSlug($name);
$slug_used_already = id(new PhabricatorProjectSlug())
->loadOneWhere('slug = %s', $slug);
if ($slug_used_already &&
$slug_used_already->getProjectPHID() != $object->getPHID()) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Duplicate'),
pht(
'Project name generates the same hashtag ("%s") as another '.
'existing project. Choose a unique name.',
'#'.$slug),
nonempty(last($xactions), null));
$errors[] = $error;
}
break;
case PhabricatorProjectTransaction::TYPE_SLUGS:
if (!$xactions) {
break;
}
$slug_xaction = last($xactions);
$new = $slug_xaction->getNewValue();
$invalid = array();
foreach ($new as $slug) {
if (!PhabricatorSlug::isValidProjectSlug($slug)) {
$invalid[] = $slug;
}
}
if ($invalid) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Hashtags must contain at least one letter or number. %s '.
'project hashtag(s) are invalid: %s.',
phutil_count($invalid),
implode(', ', $invalid)),
$slug_xaction);
break;
}
$new = $this->normalizeSlugs($new);
if ($new) {
$slugs_used_already = id(new PhabricatorProjectSlug())
->loadAllWhere('slug IN (%Ls)', $new);
} else {
// The project doesn't have any extra slugs.
$slugs_used_already = array();
}
$slugs_used_already = mgroup($slugs_used_already, 'getProjectPHID');
foreach ($slugs_used_already as $project_phid => $used_slugs) {
if ($project_phid == $object->getPHID()) {
continue;
}
$used_slug_strs = mpull($used_slugs, 'getSlug');
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'%s project hashtag(s) are already used by other projects: %s.',
phutil_count($used_slug_strs),
implode(', ', $used_slug_strs)),
$slug_xaction);
$errors[] = $error;
}
break;
case PhabricatorProjectTransaction::TYPE_PARENT:
case PhabricatorProjectTransaction::TYPE_MILESTONE:
if (!$xactions) {
break;
}
$xaction = last($xactions);
$parent_phid = $xaction->getNewValue();
if (!$parent_phid) {
continue;
}
if (!$this->getIsNewObject()) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'You can only set a parent or milestone project when creating a '.
'project for the first time.'),
$xaction);
break;
}
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->requireActor())
->withPHIDs(array($parent_phid))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
if (!$projects) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Parent or milestone project PHID ("%s") must be the PHID of a '.
'valid, visible project which you have permission to edit.',
$parent_phid),
$xaction);
break;
}
$project = head($projects);
if ($project->isMilestone()) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Parent or milestone project PHID ("%s") must not be a '.
'milestone. Milestones may not have subprojects or milestones.',
$parent_phid),
$xaction);
break;
}
$limit = PhabricatorProject::getProjectDepthLimit();
if ($project->getProjectDepth() >= ($limit - 1)) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'You can not create a subproject or mielstone under this parent '.
'because it would nest projects too deeply. The maximum '.
'nesting depth of projects is %s.',
new PhutilNumber($limit)),
$xaction);
break;
}
$object->attachParentProject($project);
break;
}
return $errors;
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_NAME:
case PhabricatorProjectTransaction::TYPE_STATUS:
case PhabricatorProjectTransaction::TYPE_IMAGE:
case PhabricatorProjectTransaction::TYPE_ICON:
case PhabricatorProjectTransaction::TYPE_COLOR:
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_EDIT);
return;
case PhabricatorProjectTransaction::TYPE_LOCKED:
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
newv($this->getEditorApplicationClass(), array()),
ProjectCanLockProjectsCapability::CAPABILITY);
return;
case PhabricatorTransactions::TYPE_EDGE:
switch ($xaction->getMetadataValue('edge:type')) {
case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST:
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$add = array_keys(array_diff_key($new, $old));
$rem = array_keys(array_diff_key($old, $new));
$actor_phid = $this->requireActor()->getPHID();
$is_join = (($add === array($actor_phid)) && !$rem);
$is_leave = (($rem === array($actor_phid)) && !$add);
if ($is_join) {
// You need CAN_JOIN to join a project.
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_JOIN);
} else if ($is_leave) {
// You usually don't need any capabilities to leave a project.
if ($object->getIsMembershipLocked()) {
// you must be able to edit though to leave locked projects
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_EDIT);
}
} else {
// You need CAN_EDIT to change members other than yourself.
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_EDIT);
}
return;
}
break;
}
return parent::requireCapabilities($object, $xaction);
}
protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
// NOTE: We're using the omnipotent user here because the original actor
// may no longer have permission to view the object.
return id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($object->getPHID()))
- ->needMembers(true)
+ ->needAncestorMembers(true)
->executeOne();
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return pht('[Project]');
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$this->getActingAsPHID(),
);
}
public function getMailTagsMap() {
return array(
PhabricatorProjectTransaction::MAILTAG_METADATA =>
pht('Project name, hashtags, icon, image, or color changes.'),
PhabricatorProjectTransaction::MAILTAG_MEMBERS =>
pht('Project membership changes.'),
PhabricatorProjectTransaction::MAILTAG_WATCHERS =>
pht('Project watcher list changes.'),
PhabricatorProjectTransaction::MAILTAG_SUBSCRIBERS =>
pht('Project subscribers change.'),
PhabricatorProjectTransaction::MAILTAG_OTHER =>
pht('Other project activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ProjectReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
->setSubject("{$name}")
->addHeader('Thread-Topic', "Project {$id}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$uri = '/project/profile/'.$object->getID().'/';
$body->addLinkSection(
pht('PROJECT DETAIL'),
PhabricatorEnv::getProductionURI($uri));
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
protected function extractFilePHIDsFromCustomTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_IMAGE:
$new = $xaction->getNewValue();
if ($new) {
return array($new);
}
break;
}
return parent::extractFilePHIDsFromCustomTransaction($object, $xaction);
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$materialize = false;
$new_parent = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
switch ($xaction->getMetadataValue('edge:type')) {
case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST:
$materialize = true;
break;
}
break;
case PhabricatorProjectTransaction::TYPE_PARENT:
$materialize = true;
$new_parent = $object->getParentProject();
break;
}
}
if ($new_parent) {
// If we just created the first subproject of this parent, we want to
// copy all of the real members to the subproject.
if (!$new_parent->getHasSubprojects()) {
$member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$project_members = PhabricatorEdgeQuery::loadDestinationPHIDs(
$new_parent->getPHID(),
$member_type);
if ($project_members) {
$editor = id(new PhabricatorEdgeEditor());
foreach ($project_members as $phid) {
$editor->addEdge($object->getPHID(), $member_type, $phid);
}
$editor->save();
}
}
}
if ($this->getIsNewObject()) {
$this->setDefaultProfilePicture($object);
}
// TODO: We should dump an informational transaction onto the parent
// project to show that we created the sub-thing.
if ($materialize) {
id(new PhabricatorProjectsMembershipIndexEngineExtension())
->rematerialize($object);
}
return parent::applyFinalEffects($object, $xactions);
}
private function addSlug(PhabricatorProject $project, $slug, $force) {
$slug = PhabricatorSlug::normalizeProjectSlug($slug);
$table = new PhabricatorProjectSlug();
$project_phid = $project->getPHID();
if ($force) {
// If we have the `$force` flag set, we only want to ignore an existing
// slug if it's for the same project. We'll error on collisions with
// other projects.
$current = $table->loadOneWhere(
'slug = %s AND projectPHID = %s',
$slug,
$project_phid);
} else {
// Without the `$force` flag, we'll just return without doing anything
// if any other project already has the slug.
$current = $table->loadOneWhere(
'slug = %s',
$slug);
}
if ($current) {
return;
}
return id(new PhabricatorProjectSlug())
->setSlug($slug)
->setProjectPHID($project_phid)
->save();
}
private function removeSlugs(PhabricatorProject $project, array $slugs) {
if (!$slugs) {
return;
}
// We're going to try to delete both the literal and normalized versions
// of all slugs. This allows us to destroy old slugs that are no longer
// valid.
foreach ($this->normalizeSlugs($slugs) as $slug) {
$slugs[] = $slug;
}
$objects = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID = %s AND slug IN (%Ls)',
$project->getPHID(),
$slugs);
foreach ($objects as $object) {
$object->delete();
}
}
private function normalizeSlugs(array $slugs) {
foreach ($slugs as $key => $slug) {
$slugs[$key] = PhabricatorSlug::normalizeProjectSlug($slug);
}
$slugs = array_unique($slugs);
$slugs = array_values($slugs);
return $slugs;
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
$type_edge = PhabricatorTransactions::TYPE_EDGE;
$edgetype_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$member_xaction = null;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() !== $type_edge) {
continue;
}
$edgetype = $xaction->getMetadataValue('edge:type');
if ($edgetype !== $edgetype_member) {
continue;
}
$member_xaction = $xaction;
}
if ($member_xaction) {
$object_phid = $object->getPHID();
if ($object_phid) {
$project = id(new PhabricatorProjectQuery())
->setViewer($this->getActor())
->withPHIDs(array($object_phid))
->needMembers(true)
->executeOne();
$members = $project->getMemberPHIDs();
} else {
$members = array();
}
$clone_xaction = clone $member_xaction;
$hint = $this->getPHIDTransactionNewValue($clone_xaction, $members);
$rule = new PhabricatorProjectMembersPolicyRule();
$hint = array_fuse($hint);
PhabricatorPolicyRule::passTransactionHintToRule(
$copy,
$rule,
$hint);
}
return $copy;
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$actor = $this->getActor();
$actor_phid = $actor->getPHID();
$results = parent::expandTransactions($object, $xactions);
// Automatically add the author as a member when they create a project
// if they're using the web interface.
$content_source = $this->getContentSource();
$source_web = PhabricatorContentSource::SOURCE_WEB;
$is_web = ($content_source->getSource() === $source_web);
if ($this->getIsNewObject() && $is_web) {
if ($actor_phid) {
$type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$results[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $type_member)
->setNewValue(
array(
'+' => array($actor_phid => $actor_phid),
));
}
}
$is_milestone = $object->isMilestone();
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_MILESTONE:
if ($xaction->getNewValue() !== null) {
$is_milestone = true;
}
break;
}
}
$this->setIsMilestone($is_milestone);
return $results;
}
private function setDefaultProfilePicture(PhabricatorProject $project) {
if ($project->isMilestone()) {
return;
}
$compose_color = $project->getDisplayIconComposeColor();
$compose_icon = $project->getDisplayIconComposeIcon();
$builtin = id(new PhabricatorFilesComposeIconBuiltinFile())
->setColor($compose_color)
->setIcon($compose_icon);
$data = $builtin->loadBuiltinFileData();
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => $builtin->getBuiltinDisplayName(),
'profile' => true,
'canCDN' => true,
));
$project
->setProfileImagePHID($file->getPHID())
->save();
}
}
diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php
index e2d678e7d7..304dd956e1 100644
--- a/src/applications/project/query/PhabricatorProjectQuery.php
+++ b/src/applications/project/query/PhabricatorProjectQuery.php
@@ -1,700 +1,722 @@
<?php
final class PhabricatorProjectQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $memberPHIDs;
private $slugs;
private $slugNormals;
private $slugMap;
private $allSlugs;
private $names;
private $nameTokens;
private $icons;
private $colors;
private $ancestorPHIDs;
private $parentPHIDs;
private $isMilestone;
private $hasSubprojects;
private $minDepth;
private $maxDepth;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_ACTIVE = 'status-active';
const STATUS_ARCHIVED = 'status-archived';
private $needSlugs;
private $needMembers;
+ private $needAncestorMembers;
private $needWatchers;
private $needImages;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withMemberPHIDs(array $member_phids) {
$this->memberPHIDs = $member_phids;
return $this;
}
public function withSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNameTokens(array $tokens) {
$this->nameTokens = array_values($tokens);
return $this;
}
public function withIcons(array $icons) {
$this->icons = $icons;
return $this;
}
public function withColors(array $colors) {
$this->colors = $colors;
return $this;
}
public function withParentProjectPHIDs($parent_phids) {
$this->parentPHIDs = $parent_phids;
return $this;
}
public function withAncestorProjectPHIDs($ancestor_phids) {
$this->ancestorPHIDs = $ancestor_phids;
return $this;
}
public function withIsMilestone($is_milestone) {
$this->isMilestone = $is_milestone;
return $this;
}
public function withHasSubprojects($has_subprojects) {
$this->hasSubprojects = $has_subprojects;
return $this;
}
public function withDepthBetween($min, $max) {
$this->minDepth = $min;
$this->maxDepth = $max;
return $this;
}
public function needMembers($need_members) {
$this->needMembers = $need_members;
return $this;
}
+ public function needAncestorMembers($need_ancestor_members) {
+ $this->needAncestorMembers = $need_ancestor_members;
+ return $this;
+ }
+
public function needWatchers($need_watchers) {
$this->needWatchers = $need_watchers;
return $this;
}
public function needImages($need_images) {
$this->needImages = $need_images;
return $this;
}
public function needSlugs($need_slugs) {
$this->needSlugs = $need_slugs;
return $this;
}
public function newResultObject() {
return new PhabricatorProject();
}
protected function getDefaultOrderVector() {
return array('name');
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'reverse' => true,
'type' => 'string',
'unique' => true,
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$project = $this->loadCursorObject($cursor);
return array(
'name' => $project->getName(),
);
}
public function getSlugMap() {
if ($this->slugMap === null) {
throw new PhutilInvalidStateException('execute');
}
return $this->slugMap;
}
protected function willExecute() {
$this->slugMap = array();
$this->slugNormals = array();
$this->allSlugs = array();
if ($this->slugs) {
foreach ($this->slugs as $slug) {
if (PhabricatorSlug::isValidProjectSlug($slug)) {
$normal = PhabricatorSlug::normalizeProjectSlug($slug);
$this->slugNormals[$slug] = $normal;
$this->allSlugs[$normal] = $normal;
}
// NOTE: At least for now, we query for the normalized slugs but also
// for the slugs exactly as entered. This allows older projects with
// slugs that are no longer valid to continue to work.
$this->allSlugs[$slug] = $slug;
}
}
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $projects) {
$ancestor_paths = array();
foreach ($projects as $project) {
foreach ($project->getAncestorProjectPaths() as $path) {
$ancestor_paths[$path] = $path;
}
}
if ($ancestor_paths) {
$ancestors = id(new PhabricatorProject())->loadAllWhere(
'projectPath IN (%Ls)',
$ancestor_paths);
} else {
$ancestors = array();
}
$projects = $this->linkProjectGraph($projects, $ancestors);
$viewer_phid = $this->getViewer()->getPHID();
$material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
$watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
$types = array();
$types[] = $material_type;
if ($this->needWatchers) {
$types[] = $watcher_type;
}
+ $all_graph = $this->getAllReachableAncestors($projects);
+
+ if ($this->needAncestorMembers) {
+ $src_projects = $all_graph;
+ } else {
+ $src_projects = $projects;
+ }
+
$all_sources = array();
- foreach ($projects as $project) {
+ foreach ($src_projects as $project) {
if ($project->isMilestone()) {
$phid = $project->getParentProjectPHID();
} else {
$phid = $project->getPHID();
}
$all_sources[$phid] = $phid;
}
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($all_sources)
->withEdgeTypes($types);
+ $need_all_edges =
+ $this->needMembers ||
+ $this->needWatchers ||
+ $this->needAncestorMembers;
+
// If we only need to know if the viewer is a member, we can restrict
// the query to just their PHID.
$any_edges = true;
- if (!$this->needMembers && !$this->needWatchers) {
+ if (!$need_all_edges) {
if ($viewer_phid) {
$edge_query->withDestinationPHIDs(array($viewer_phid));
} else {
// If we don't need members or watchers and don't have a viewer PHID
// (viewer is logged-out or omnipotent), they'll never be a member
// so we don't need to issue this query at all.
$any_edges = false;
}
}
if ($any_edges) {
$edge_query->execute();
}
$membership_projects = array();
- foreach ($projects as $project) {
+ foreach ($src_projects as $project) {
$project_phid = $project->getPHID();
if ($project->isMilestone()) {
$source_phids = array($project->getParentProjectPHID());
} else {
$source_phids = array($project_phid);
}
if ($any_edges) {
$member_phids = $edge_query->getDestinationPHIDs(
$source_phids,
array($material_type));
} else {
$member_phids = array();
}
if (in_array($viewer_phid, $member_phids)) {
$membership_projects[$project_phid] = $project;
}
- if ($this->needMembers) {
+ if ($this->needMembers || $this->needAncestorMembers) {
$project->attachMemberPHIDs($member_phids);
}
if ($this->needWatchers) {
$watcher_phids = $edge_query->getDestinationPHIDs(
$source_phids,
array($watcher_type));
$project->attachWatcherPHIDs($watcher_phids);
$project->setIsUserWatcher(
$viewer_phid,
in_array($viewer_phid, $watcher_phids));
}
}
- $all_graph = $this->getAllReachableAncestors($projects);
- $member_graph = $this->getAllReachableAncestors($membership_projects);
+ // If we loaded ancestor members, we've already populated membership
+ // lists above, so we can skip this step.
+ if (!$this->needAncestorMembers) {
+ $member_graph = $this->getAllReachableAncestors($membership_projects);
- foreach ($all_graph as $phid => $project) {
- $is_member = isset($member_graph[$phid]);
- $project->setIsUserMember($viewer_phid, $is_member);
+ foreach ($all_graph as $phid => $project) {
+ $is_member = isset($member_graph[$phid]);
+ $project->setIsUserMember($viewer_phid, $is_member);
+ }
}
return $projects;
}
protected function didFilterPage(array $projects) {
if ($this->needImages) {
$default = null;
$file_phids = mpull($projects, 'getProfileImagePHID');
$file_phids = array_filter($file_phids);
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
} else {
$files = array();
}
foreach ($projects as $project) {
$file = idx($files, $project->getProfileImagePHID());
if (!$file) {
if (!$default) {
$default = PhabricatorFile::loadBuiltin(
$this->getViewer(),
'project.png');
}
$file = $default;
}
$project->attachProfileImageFile($file);
}
}
$this->loadSlugs($projects);
return $projects;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->status != self::STATUS_ANY) {
switch ($this->status) {
case self::STATUS_OPEN:
case self::STATUS_ACTIVE:
$filter = array(
PhabricatorProjectStatus::STATUS_ACTIVE,
);
break;
case self::STATUS_CLOSED:
case self::STATUS_ARCHIVED:
$filter = array(
PhabricatorProjectStatus::STATUS_ARCHIVED,
);
break;
default:
throw new Exception(
pht(
"Unknown project status '%s'!",
$this->status));
}
$where[] = qsprintf(
$conn,
'status IN (%Ld)',
$filter);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->memberPHIDs !== null) {
$where[] = qsprintf(
$conn,
'e.dst IN (%Ls)',
$this->memberPHIDs);
}
if ($this->slugs !== null) {
$where[] = qsprintf(
$conn,
'slug.slug IN (%Ls)',
$this->allSlugs);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'name IN (%Ls)',
$this->names);
}
if ($this->icons !== null) {
$where[] = qsprintf(
$conn,
'icon IN (%Ls)',
$this->icons);
}
if ($this->colors !== null) {
$where[] = qsprintf(
$conn,
'color IN (%Ls)',
$this->colors);
}
if ($this->parentPHIDs !== null) {
$where[] = qsprintf(
$conn,
'parentProjectPHID IN (%Ls)',
$this->parentPHIDs);
}
if ($this->ancestorPHIDs !== null) {
$ancestor_paths = queryfx_all(
$conn,
'SELECT projectPath, projectDepth FROM %T WHERE phid IN (%Ls)',
id(new PhabricatorProject())->getTableName(),
$this->ancestorPHIDs);
if (!$ancestor_paths) {
throw new PhabricatorEmptyQueryException();
}
$sql = array();
foreach ($ancestor_paths as $ancestor_path) {
$sql[] = qsprintf(
$conn,
'(projectPath LIKE %> AND projectDepth > %d)',
$ancestor_path['projectPath'],
$ancestor_path['projectDepth']);
}
$where[] = '('.implode(' OR ', $sql).')';
$where[] = qsprintf(
$conn,
'parentProjectPHID IS NOT NULL');
}
if ($this->isMilestone !== null) {
if ($this->isMilestone) {
$where[] = qsprintf(
$conn,
'milestoneNumber IS NOT NULL');
} else {
$where[] = qsprintf(
$conn,
'milestoneNumber IS NULL');
}
}
if ($this->hasSubprojects !== null) {
$where[] = qsprintf(
$conn,
'hasSubprojects = %d',
(int)$this->hasSubprojects);
}
if ($this->minDepth !== null) {
$where[] = qsprintf(
$conn,
'projectDepth >= %d',
$this->minDepth);
}
if ($this->maxDepth !== null) {
$where[] = qsprintf(
$conn,
'projectDepth <= %d',
$this->maxDepth);
}
return $where;
}
protected function shouldGroupQueryResultRows() {
if ($this->memberPHIDs || $this->nameTokens) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->memberPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T e ON e.src = p.phid AND e.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectMaterializedMemberEdgeType::EDGECONST);
}
if ($this->slugs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T slug on slug.projectPHID = p.phid',
id(new PhabricatorProjectSlug())->getTableName());
}
if ($this->nameTokens !== null) {
foreach ($this->nameTokens as $key => $token) {
$token_table = 'token_'.$key;
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>',
PhabricatorProject::TABLE_DATASOURCE_TOKEN,
$token_table,
$token_table,
$token_table,
$token);
}
}
return $joins;
}
public function getQueryApplicationClass() {
return 'PhabricatorProjectApplication';
}
protected function getPrimaryTableAlias() {
return 'p';
}
private function linkProjectGraph(array $projects, array $ancestors) {
$ancestor_map = mpull($ancestors, null, 'getPHID');
$projects_map = mpull($projects, null, 'getPHID');
$all_map = $projects_map + $ancestor_map;
$done = array();
foreach ($projects as $key => $project) {
$seen = array($project->getPHID() => true);
if (!$this->linkProject($project, $all_map, $done, $seen)) {
$this->didRejectResult($project);
unset($projects[$key]);
continue;
}
foreach ($project->getAncestorProjects() as $ancestor) {
$seen[$ancestor->getPHID()] = true;
}
}
return $projects;
}
private function linkProject($project, array $all, array $done, array $seen) {
$parent_phid = $project->getParentProjectPHID();
// This project has no parent, so just attach `null` and return.
if (!$parent_phid) {
$project->attachParentProject(null);
return true;
}
// This project has a parent, but it failed to load.
if (empty($all[$parent_phid])) {
return false;
}
// Test for graph cycles. If we encounter one, we're going to hide the
// entire cycle since we can't meaningfully resolve it.
if (isset($seen[$parent_phid])) {
return false;
}
$seen[$parent_phid] = true;
$parent = $all[$parent_phid];
$project->attachParentProject($parent);
if (!empty($done[$parent_phid])) {
return true;
}
return $this->linkProject($parent, $all, $done, $seen);
}
private function getAllReachableAncestors(array $projects) {
$ancestors = array();
$seen = mpull($projects, null, 'getPHID');
$stack = $projects;
while ($stack) {
$project = array_pop($stack);
$phid = $project->getPHID();
$ancestors[$phid] = $project;
$parent_phid = $project->getParentProjectPHID();
if (!$parent_phid) {
continue;
}
if (isset($seen[$parent_phid])) {
continue;
}
$seen[$parent_phid] = true;
$stack[] = $project->getParentProject();
}
return $ancestors;
}
private function loadSlugs(array $projects) {
// Build a map from primary slugs to projects.
$primary_map = array();
foreach ($projects as $project) {
$primary_slug = $project->getPrimarySlug();
if ($primary_slug === null) {
continue;
}
$primary_map[$primary_slug] = $project;
}
// Link up all of the queried slugs which correspond to primary
// slugs. If we can link up everything from this (no slugs were queried,
// or only primary slugs were queried) we don't need to load anything
// else.
$unknown = $this->slugNormals;
foreach ($unknown as $input => $normal) {
if (isset($primary_map[$input])) {
$match = $input;
} else if (isset($primary_map[$normal])) {
$match = $normal;
} else {
continue;
}
$this->slugMap[$input] = array(
'slug' => $match,
'projectPHID' => $primary_map[$match]->getPHID(),
);
unset($unknown[$input]);
}
// If we need slugs, we have to load everything.
// If we still have some queried slugs which we haven't mapped, we only
// need to look for them.
// If we've mapped everything, we don't have to do any work.
$project_phids = mpull($projects, 'getPHID');
if ($this->needSlugs) {
$slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID IN (%Ls)',
$project_phids);
} else if ($unknown) {
$slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID IN (%Ls) AND slug IN (%Ls)',
$project_phids,
$unknown);
} else {
$slugs = array();
}
// Link up any slugs we were not able to link up earlier.
$extra_map = mpull($slugs, 'getProjectPHID', 'getSlug');
foreach ($unknown as $input => $normal) {
if (isset($extra_map[$input])) {
$match = $input;
} else if (isset($extra_map[$normal])) {
$match = $normal;
} else {
continue;
}
$this->slugMap[$input] = array(
'slug' => $match,
'projectPHID' => $extra_map[$match],
);
unset($unknown[$input]);
}
if ($this->needSlugs) {
$slug_groups = mgroup($slugs, 'getProjectPHID');
foreach ($projects as $project) {
$project_slugs = idx($slug_groups, $project->getPHID(), array());
$project->attachSlugs($project_slugs);
}
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jan 19, 13:38 (3 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1125141
Default Alt Text
(80 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment