1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-27 01:02:42 +01:00

Migrate Project slugs to modular transactions

Test Plan: Unit tests all pass. Added/removed/altered some project hashtags and observed expected transactions in timeline.

Reviewers: #blessed_reviewers, epriestley

Reviewed By: #blessed_reviewers, epriestley

Subscribers: epriestley

Differential Revision: https://secure.phabricator.com/D17952
This commit is contained in:
Austin McKinley 2017-05-18 11:03:31 -07:00
parent 1599c56217
commit 91eb22cb3a
7 changed files with 181 additions and 157 deletions

View file

@ -3666,6 +3666,7 @@ phutil_register_library_map(array(
'PhabricatorProjectSilenceController' => 'applications/project/controller/PhabricatorProjectSilenceController.php', 'PhabricatorProjectSilenceController' => 'applications/project/controller/PhabricatorProjectSilenceController.php',
'PhabricatorProjectSilencedEdgeType' => 'applications/project/edge/PhabricatorProjectSilencedEdgeType.php', 'PhabricatorProjectSilencedEdgeType' => 'applications/project/edge/PhabricatorProjectSilencedEdgeType.php',
'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php', 'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php',
'PhabricatorProjectSlugsTransaction' => 'applications/project/xaction/PhabricatorProjectSlugsTransaction.php',
'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php', 'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php',
'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php', 'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php',
'PhabricatorProjectSubprojectWarningController' => 'applications/project/controller/PhabricatorProjectSubprojectWarningController.php', 'PhabricatorProjectSubprojectWarningController' => 'applications/project/controller/PhabricatorProjectSubprojectWarningController.php',
@ -9077,6 +9078,7 @@ phutil_register_library_map(array(
'PhabricatorProjectSilenceController' => 'PhabricatorProjectController', 'PhabricatorProjectSilenceController' => 'PhabricatorProjectController',
'PhabricatorProjectSilencedEdgeType' => 'PhabricatorEdgeType', 'PhabricatorProjectSilencedEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectSlug' => 'PhabricatorProjectDAO', 'PhabricatorProjectSlug' => 'PhabricatorProjectDAO',
'PhabricatorProjectSlugsTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectStandardCustomField' => array( 'PhabricatorProjectStandardCustomField' => array(
'PhabricatorProjectCustomField', 'PhabricatorProjectCustomField',
'PhabricatorStandardCustomFieldInterface', 'PhabricatorStandardCustomFieldInterface',

View file

@ -362,7 +362,7 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
$xactions = array(); $xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction()) $xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS) ->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($name)); ->setNewValue(array($name));
$this->applyTransactions($project, $user, $xactions); $this->applyTransactions($project, $user, $xactions);
@ -386,7 +386,7 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
->setNewValue($name2); ->setNewValue($name2);
$xactions[] = id(new PhabricatorProjectTransaction()) $xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS) ->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($name2)); ->setNewValue(array($name2));
$this->applyTransactions($project, $user, $xactions); $this->applyTransactions($project, $user, $xactions);
@ -416,7 +416,7 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
$xactions = array(); $xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction()) $xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS) ->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($input, $input)); ->setNewValue(array($input, $input));
$this->applyTransactions($project, $user, $xactions); $this->applyTransactions($project, $user, $xactions);
@ -448,7 +448,7 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
$xactions = array(); $xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction()) $xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS) ->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($input)); ->setNewValue(array($input));
$this->applyTransactions($project, $user, $xactions); $this->applyTransactions($project, $user, $xactions);
@ -474,7 +474,7 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
$xactions = array(); $xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction()) $xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS) ->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($input)); ->setNewValue(array($input));
$caught = null; $caught = null;
@ -605,7 +605,7 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
->setNewValue($name); ->setNewValue($name);
$xactions[] = id(new PhabricatorProjectTransaction()) $xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS) ->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($slug)); ->setNewValue(array($slug));
$this->applyTransactions($project, $user, $xactions); $this->applyTransactions($project, $user, $xactions);

View file

@ -64,7 +64,8 @@ final class ProjectCreateConduitAPIMethod extends ProjectConduitAPIMethod {
if ($request->getValue('tags')) { if ($request->getValue('tags')) {
$xactions[] = id(new PhabricatorProjectTransaction()) $xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS) ->setTransactionType(
PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue($request->getValue('tags')); ->setNewValue($request->getValue('tags'));
} }

View file

@ -30,7 +30,6 @@ final class PhabricatorProjectTransactionEditor
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PhabricatorTransactions::TYPE_JOIN_POLICY; $types[] = PhabricatorTransactions::TYPE_JOIN_POLICY;
$types[] = PhabricatorProjectTransaction::TYPE_SLUGS;
$types[] = PhabricatorProjectTransaction::TYPE_STATUS; $types[] = PhabricatorProjectTransaction::TYPE_STATUS;
$types[] = PhabricatorProjectTransaction::TYPE_IMAGE; $types[] = PhabricatorProjectTransaction::TYPE_IMAGE;
$types[] = PhabricatorProjectTransaction::TYPE_ICON; $types[] = PhabricatorProjectTransaction::TYPE_ICON;
@ -51,11 +50,6 @@ final class PhabricatorProjectTransactionEditor
PhabricatorApplicationTransaction $xaction) { PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) { switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_SLUGS:
$slugs = $object->getSlugs();
$slugs = mpull($slugs, 'getSlug', 'getSlug');
unset($slugs[$object->getPrimarySlug()]);
return array_keys($slugs);
case PhabricatorProjectTransaction::TYPE_STATUS: case PhabricatorProjectTransaction::TYPE_STATUS:
return $object->getStatus(); return $object->getStatus();
case PhabricatorProjectTransaction::TYPE_IMAGE: case PhabricatorProjectTransaction::TYPE_IMAGE:
@ -105,8 +99,6 @@ final class PhabricatorProjectTransactionEditor
return null; return null;
} }
return $value; return $value;
case PhabricatorProjectTransaction::TYPE_SLUGS:
return $this->normalizeSlugs($xaction->getNewValue());
} }
return parent::getCustomTransactionNewValue($object, $xaction); return parent::getCustomTransactionNewValue($object, $xaction);
@ -117,8 +109,6 @@ final class PhabricatorProjectTransactionEditor
PhabricatorApplicationTransaction $xaction) { PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) { switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_SLUGS:
return;
case PhabricatorProjectTransaction::TYPE_STATUS: case PhabricatorProjectTransaction::TYPE_STATUS:
$object->setStatus($xaction->getNewValue()); $object->setStatus($xaction->getNewValue());
return; return;
@ -167,18 +157,6 @@ final class PhabricatorProjectTransactionEditor
$new = $xaction->getNewValue(); $new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) { switch ($xaction->getTransactionType()) {
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_STATUS:
case PhabricatorProjectTransaction::TYPE_IMAGE: case PhabricatorProjectTransaction::TYPE_IMAGE:
case PhabricatorProjectTransaction::TYPE_ICON: case PhabricatorProjectTransaction::TYPE_ICON:
@ -278,65 +256,6 @@ final class PhabricatorProjectTransactionEditor
$errors = parent::validateTransaction($object, $type, $xactions); $errors = parent::validateTransaction($object, $type, $xactions);
switch ($type) { switch ($type) {
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_PARENT:
case PhabricatorProjectTransaction::TYPE_MILESTONE: case PhabricatorProjectTransaction::TYPE_MILESTONE:
if (!$xactions) { if (!$xactions) {
@ -674,7 +593,7 @@ final class PhabricatorProjectTransactionEditor
->save(); ->save();
} }
private function removeSlugs(PhabricatorProject $project, array $slugs) { public function removeSlugs(PhabricatorProject $project, array $slugs) {
if (!$slugs) { if (!$slugs) {
return; return;
} }
@ -696,7 +615,7 @@ final class PhabricatorProjectTransactionEditor
} }
} }
private function normalizeSlugs(array $slugs) { public function normalizeSlugs(array $slugs) {
foreach ($slugs as $key => $slug) { foreach ($slugs as $key => $slug) {
$slugs[$key] = PhabricatorSlug::normalizeProjectSlug($slug); $slugs[$key] = PhabricatorSlug::normalizeProjectSlug($slug);
} }

View file

@ -262,7 +262,8 @@ final class PhabricatorProjectEditEngine
id(new PhabricatorStringListEditField()) id(new PhabricatorStringListEditField())
->setKey('slugs') ->setKey('slugs')
->setLabel(pht('Additional Hashtags')) ->setLabel(pht('Additional Hashtags'))
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS) ->setTransactionType(
PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setDescription(pht('Additional project slugs.')) ->setDescription(pht('Additional project slugs.'))
->setConduitDescription(pht('Change project slugs.')) ->setConduitDescription(pht('Change project slugs.'))
->setConduitTypeDescription(pht('New list of slugs.')) ->setConduitTypeDescription(pht('New list of slugs.'))

View file

@ -3,7 +3,6 @@
final class PhabricatorProjectTransaction final class PhabricatorProjectTransaction
extends PhabricatorModularTransaction { extends PhabricatorModularTransaction {
const TYPE_SLUGS = 'project:slugs';
const TYPE_STATUS = 'project:status'; const TYPE_STATUS = 'project:status';
const TYPE_IMAGE = 'project:image'; const TYPE_IMAGE = 'project:image';
const TYPE_ICON = 'project:icon'; const TYPE_ICON = 'project:icon';
@ -134,8 +133,6 @@ final class PhabricatorProjectTransaction
return 'fa-photo'; return 'fa-photo';
case self::TYPE_MEMBERS: case self::TYPE_MEMBERS:
return 'fa-user'; return 'fa-user';
case self::TYPE_SLUGS:
return 'fa-tag';
} }
return parent::getIcon(); return parent::getIcon();
} }
@ -212,33 +209,6 @@ final class PhabricatorProjectTransaction
} }
break; break;
case self::TYPE_SLUGS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return pht(
'%s changed project hashtag(s), added %d: %s; removed %d: %s.',
$author_handle,
count($add),
$this->renderSlugList($add),
count($rem),
$this->renderSlugList($rem));
} else if ($add) {
return pht(
'%s added %d project hashtag(s): %s.',
$author_handle,
count($add),
$this->renderSlugList($add));
} else if ($rem) {
return pht(
'%s removed %d project hashtag(s): %s.',
$author_handle,
count($rem),
$this->renderSlugList($rem));
}
break;
case self::TYPE_MEMBERS: case self::TYPE_MEMBERS:
$add = array_diff($new, $old); $add = array_diff($new, $old);
$rem = array_diff($old, $new); $rem = array_diff($old, $new);
@ -380,36 +350,6 @@ final class PhabricatorProjectTransaction
$author_handle, $author_handle,
$object_handle); $object_handle);
} }
case self::TYPE_SLUGS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return pht(
'%s changed %s hashtag(s), added %d: %s; removed %d: %s.',
$author_handle,
$object_handle,
count($add),
$this->renderSlugList($add),
count($rem),
$this->renderSlugList($rem));
} else if ($add) {
return pht(
'%s added %d %s hashtag(s): %s.',
$author_handle,
count($add),
$object_handle,
$this->renderSlugList($add));
} else if ($rem) {
return pht(
'%s removed %d %s hashtag(s): %s.',
$author_handle,
count($rem),
$object_handle,
$this->renderSlugList($rem));
}
} }
return parent::getTitleForFeed(); return parent::getTitleForFeed();
@ -418,8 +358,8 @@ final class PhabricatorProjectTransaction
public function getMailTags() { public function getMailTags() {
$tags = array(); $tags = array();
switch ($this->getTransactionType()) { switch ($this->getTransactionType()) {
case self::TYPE_NAME: case PhabricatorProjectNameTransaction::TRANSACTIONTYPE:
case self::TYPE_SLUGS: case PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE:
case self::TYPE_IMAGE: case self::TYPE_IMAGE:
case self::TYPE_ICON: case self::TYPE_ICON:
case self::TYPE_COLOR: case self::TYPE_COLOR:
@ -447,8 +387,4 @@ final class PhabricatorProjectTransaction
return $tags; return $tags;
} }
private function renderSlugList($slugs) {
return implode(', ', $slugs);
}
} }

View file

@ -0,0 +1,165 @@
<?php
final class PhabricatorProjectSlugsTransaction
extends PhabricatorProjectTransactionType {
const TRANSACTIONTYPE = 'project:slugs';
public function generateOldValue($object) {
$slugs = $object->getSlugs();
$slugs = mpull($slugs, 'getSlug', 'getSlug');
unset($slugs[$object->getPrimarySlug()]);
return array_keys($slugs);
}
public function generateNewValue($object, $value) {
return $this->getEditor()->normalizeSlugs($value);
}
public function applyInternalEffects($object, $value) {
return;
}
public function applyExternalEffects($object, $value) {
$old = $this->getOldValue();
$new = $value;
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
foreach ($add as $slug) {
$this->getEditor()->addSlug($object, $slug, true);
}
$this->getEditor()->removeSlugs($object, $rem);
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return pht(
'%s changed project hashtag(s), added %d: %s; removed %d: %s.',
$this->renderAuthor(),
count($add),
$this->renderSlugList($add),
count($rem),
$this->renderSlugList($rem));
} else if ($add) {
return pht(
'%s added %d project hashtag(s): %s.',
$this->renderAuthor(),
count($add),
$this->renderSlugList($add));
} else if ($rem) {
return pht(
'%s removed %d project hashtag(s): %s.',
$this->renderAuthor(),
count($rem),
$this->renderSlugList($rem));
}
break;
}
public function getTitleForFeed() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return pht(
'%s changed %s hashtag(s), added %d: %s; removed %d: %s.',
$this->renderAuthor(),
$this->renderObject(),
count($add),
$this->renderSlugList($add),
count($rem),
$this->renderSlugList($rem));
} else if ($add) {
return pht(
'%s added %d %s hashtag(s): %s.',
$this->renderAuthor(),
count($add),
$this->renderObject(),
$this->renderSlugList($add));
} else if ($rem) {
return pht(
'%s removed %d %s hashtag(s): %s.',
$this->renderAuthor(),
count($rem),
$this->renderObject(),
$this->renderSlugList($rem));
}
}
public function getIcon() {
return 'fa-tag';
}
public function validateTransactions($object, array $xactions) {
$errors = array();
if (!$xactions) {
return $errors;
}
$slug_xaction = last($xactions);
$new = $slug_xaction->getNewValue();
$invalid = array();
foreach ($new as $slug) {
if (!PhabricatorSlug::isValidProjectSlug($slug)) {
$invalid[] = $slug;
}
}
if ($invalid) {
$errors[] = $this->newInvalidError(
pht(
'Hashtags must contain at least one letter or number. %s '.
'project hashtag(s) are invalid: %s.',
phutil_count($invalid),
implode(', ', $invalid)));
return $errors;
}
$new = $this->getEditor()->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');
$errors[] = $this->newInvalidError(
pht(
'%s project hashtag(s) are already used by other projects: %s.',
phutil_count($used_slug_strs),
implode(', ', $used_slug_strs)));
}
return $errors;
}
private function renderSlugList($slugs) {
return implode(', ', $slugs);
}
}