diff --git a/resources/sql/patches/100.projectxaction.sql b/resources/sql/patches/100.projectxaction.sql new file mode 100644 index 0000000000..76d51f6892 --- /dev/null +++ b/resources/sql/patches/100.projectxaction.sql @@ -0,0 +1,11 @@ +CREATE TABLE phabricator_project.project_transaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + projectID INT UNSIGNED NOT NULL, + authorPHID VARCHAR(64) BINARY NOT NULL, + transactionType VARCHAR(32) NOT NULL, + oldValue LONGBLOB NOT NULL, + newValue LONGBLOB NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + KEY (projectID) +) ENGINE=InnoDB; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2a011014a0..548dbfc977 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -485,6 +485,7 @@ phutil_register_library_map(array( 'PhabricatorFeedStoryDifferential' => 'applications/feed/story/differential', 'PhabricatorFeedStoryManiphest' => 'applications/feed/story/maniphest', 'PhabricatorFeedStoryPhriction' => 'applications/feed/story/phriction', + 'PhabricatorFeedStoryProject' => 'applications/feed/story/project', 'PhabricatorFeedStoryPublisher' => 'applications/feed/publisher', 'PhabricatorFeedStoryReference' => 'applications/feed/storage/storyreference', 'PhabricatorFeedStoryStatus' => 'applications/feed/story/status', @@ -611,6 +612,7 @@ phutil_register_library_map(array( 'PhabricatorProject' => 'applications/project/storage/project', 'PhabricatorProjectAffiliation' => 'applications/project/storage/affiliation', 'PhabricatorProjectAffiliationEditController' => 'applications/project/controller/editaffiliation', + 'PhabricatorProjectConstants' => 'applications/project/constants/base', 'PhabricatorProjectController' => 'applications/project/controller/base', 'PhabricatorProjectCreateController' => 'applications/project/controller/create', 'PhabricatorProjectDAO' => 'applications/project/storage/base', @@ -623,6 +625,8 @@ phutil_register_library_map(array( 'PhabricatorProjectQuery' => 'applications/project/query/project', 'PhabricatorProjectStatus' => 'applications/project/constants/status', 'PhabricatorProjectSubproject' => 'applications/project/storage/subproject', + 'PhabricatorProjectTransaction' => 'applications/project/storage/transaction', + 'PhabricatorProjectTransactionType' => 'applications/project/constants/transaction', 'PhabricatorRedirectController' => 'applications/base/controller/redirect', 'PhabricatorRefreshCSRFController' => 'applications/auth/controller/refresh', 'PhabricatorRemarkupRuleDifferential' => 'infrastructure/markup/remarkup/markuprule/differential', @@ -1192,6 +1196,7 @@ phutil_register_library_map(array( 'PhabricatorFeedStoryDifferential' => 'PhabricatorFeedStory', 'PhabricatorFeedStoryManiphest' => 'PhabricatorFeedStory', 'PhabricatorFeedStoryPhriction' => 'PhabricatorFeedStory', + 'PhabricatorFeedStoryProject' => 'PhabricatorFeedStory', 'PhabricatorFeedStoryReference' => 'PhabricatorFeedDAO', 'PhabricatorFeedStoryStatus' => 'PhabricatorFeedStory', 'PhabricatorFeedStoryTypeConstants' => 'PhabricatorFeedConstants', @@ -1304,6 +1309,8 @@ phutil_register_library_map(array( 'PhabricatorProjectProfileController' => 'PhabricatorProjectController', 'PhabricatorProjectProfileEditController' => 'PhabricatorProjectController', 'PhabricatorProjectSubproject' => 'PhabricatorProjectDAO', + 'PhabricatorProjectTransaction' => 'PhabricatorProjectDAO', + 'PhabricatorProjectTransactionType' => 'PhabricatorProjectConstants', 'PhabricatorRedirectController' => 'PhabricatorController', 'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController', 'PhabricatorRemarkupRuleDifferential' => 'PhabricatorRemarkupRuleObjectName', diff --git a/src/applications/feed/constants/story/PhabricatorFeedStoryTypeConstants.php b/src/applications/feed/constants/story/PhabricatorFeedStoryTypeConstants.php index 4ab63dd596..0a9de66251 100644 --- a/src/applications/feed/constants/story/PhabricatorFeedStoryTypeConstants.php +++ b/src/applications/feed/constants/story/PhabricatorFeedStoryTypeConstants.php @@ -1,7 +1,7 @@ getStoryData()->getAuthorPHID(), + $this->getStoryData()->getValue('projectPHID'), + ); + } + + public function getRequiredObjectPHIDs() { + return array( + $this->getStoryData()->getAuthorPHID(), + ); + } + + public function renderView() { + $data = $this->getStoryData(); + + $view = new PhabricatorFeedStoryView(); + + $type = $data->getValue('type'); + $old = $data->getValue('old'); + $new = $data->getValue('new'); + $proj = $this->getHandle($data->getValue('projectPHID')); + $auth = $this->getHandle($data->getAuthorPHID()); + + switch ($type) { + case PhabricatorProjectTransactionType::TYPE_NAME: + if (strlen($old)) { + $action = 'renamed project '. + ''.$proj->renderLink().''. + ' from '. + ''.phutil_escape_html($old).''. + ' to '. + ''.phutil_escape_html($new).'.'; + } else { + $action = 'created project '. + ''.$proj->renderLink().''. + ' (as '. + ''.phutil_escape_html($new).').'; + } + break; + default: + $action = 'updated project '.$proj->renderLink().''; + break; + } + $view->setTitle(''.$auth->renderLink().' '.$action); + $view->setOneLineStory(true); + + return $view; + } + +} diff --git a/src/applications/feed/story/project/__init__.php b/src/applications/feed/story/project/__init__.php new file mode 100644 index 0000000000..457b290fdf --- /dev/null +++ b/src/applications/feed/story/project/__init__.php @@ -0,0 +1,16 @@ +isFormPost()) { try { + $xactions = array(); + $xaction = new PhabricatorProjectTransaction(); + $xaction->setTransactionType( + PhabricatorProjectTransactionType::TYPE_NAME); + $xaction->setNewValue($request->getStr('name')); + $xactions[] = $xaction; + $editor = new PhabricatorProjectEditor($project); $editor->setUser($user); - $editor->setName($request->getStr('name')); - $editor->save(); + $editor->applyTransactions($xactions); } catch (PhabricatorProjectNameCollisionException $ex) { $e_name = 'Not Unique'; $errors[] = $ex->getMessage(); diff --git a/src/applications/project/controller/create/__init__.php b/src/applications/project/controller/create/__init__.php index e8dccac003..074eac7b97 100644 --- a/src/applications/project/controller/create/__init__.php +++ b/src/applications/project/controller/create/__init__.php @@ -10,11 +10,13 @@ phutil_require_module('phabricator', 'aphront/response/ajax'); phutil_require_module('phabricator', 'aphront/response/dialog'); phutil_require_module('phabricator', 'aphront/response/redirect'); phutil_require_module('phabricator', 'applications/project/constants/status'); +phutil_require_module('phabricator', 'applications/project/constants/transaction'); phutil_require_module('phabricator', 'applications/project/controller/base'); phutil_require_module('phabricator', 'applications/project/editor/project'); phutil_require_module('phabricator', 'applications/project/storage/affiliation'); phutil_require_module('phabricator', 'applications/project/storage/profile'); phutil_require_module('phabricator', 'applications/project/storage/project'); +phutil_require_module('phabricator', 'applications/project/storage/transaction'); phutil_require_module('phabricator', 'view/dialog'); phutil_require_module('phabricator', 'view/form/base'); phutil_require_module('phabricator', 'view/form/control/submit'); diff --git a/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php b/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php index 265048a0f7..3f609692cb 100644 --- a/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php +++ b/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php @@ -58,10 +58,16 @@ class PhabricatorProjectProfileEditController if ($request->isFormPost()) { try { + $xactions = array(); + $xaction = new PhabricatorProjectTransaction(); + $xaction->setTransactionType( + PhabricatorProjectTransactionType::TYPE_NAME); + $xaction->setNewValue($request->getStr('name')); + $xactions[] = $xaction; + $editor = new PhabricatorProjectEditor($project); $editor->setUser($user); - $editor->setName($request->getStr('name')); - $editor->save(); + $editor->applyTransactions($xactions); } catch (PhabricatorProjectNameCollisionException $ex) { $e_name = 'Not Unique'; $errors[] = $ex->getMessage(); diff --git a/src/applications/project/controller/profileedit/__init__.php b/src/applications/project/controller/profileedit/__init__.php index df824b9bb9..9bb8a8d434 100644 --- a/src/applications/project/controller/profileedit/__init__.php +++ b/src/applications/project/controller/profileedit/__init__.php @@ -12,11 +12,13 @@ phutil_require_module('phabricator', 'applications/files/storage/file'); phutil_require_module('phabricator', 'applications/files/transform'); phutil_require_module('phabricator', 'applications/phid/handle/data'); phutil_require_module('phabricator', 'applications/project/constants/status'); +phutil_require_module('phabricator', 'applications/project/constants/transaction'); phutil_require_module('phabricator', 'applications/project/controller/base'); phutil_require_module('phabricator', 'applications/project/editor/project'); phutil_require_module('phabricator', 'applications/project/storage/affiliation'); phutil_require_module('phabricator', 'applications/project/storage/profile'); phutil_require_module('phabricator', 'applications/project/storage/project'); +phutil_require_module('phabricator', 'applications/project/storage/transaction'); phutil_require_module('phabricator', 'infrastructure/celerity/api'); phutil_require_module('phabricator', 'infrastructure/javelin/api'); phutil_require_module('phabricator', 'infrastructure/javelin/markup'); diff --git a/src/applications/project/editor/project/PhabricatorProjectEditor.php b/src/applications/project/editor/project/PhabricatorProjectEditor.php index a4338938dd..6865cd7fad 100644 --- a/src/applications/project/editor/project/PhabricatorProjectEditor.php +++ b/src/applications/project/editor/project/PhabricatorProjectEditor.php @@ -1,7 +1,7 @@ project = $project; } - public function setName($name) { - $this->projectName = $name; - return $this; - } - public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } - public function save() { + public function applyTransactions(array $transactions) { if (!$this->user) { throw new Exception('Call setUser() before save()!'); } + $user = $this->user; $project = $this->project; $is_new = !$project->getID(); if ($is_new) { - $project->setAuthorPHID($this->user->getPHID()); + $project->setAuthorPHID($user->getPHID()); } - if (($this->projectName !== null) && - ($this->projectName !== $project->getName())) { - $project->setName($this->projectName); - $project->setPhrictionSlug($this->projectName); - $this->validateName($project); + foreach ($transactions as $xaction) { + $type = $xaction->getTransactionType(); + + $this->setTransactionOldValue($project, $xaction); + $this->applyTransactionEffect($project, $xaction); + } try { $project->save(); + + foreach ($transactions as $xaction) { + $xaction->setAuthorPHID($user->getPHID()); + $xaction->setProjectID($project->getID()); + $xaction->save(); + } + + foreach ($this->remAffiliations as $affil) { + $affil->delete(); + } + + foreach ($this->addAffiliations as $affil) { + $affil->setProjectPHID($project->getPHID()); + $affil->save(); + } + + foreach ($transactions as $xaction) { + $this->publishTransactionStory($project, $xaction); + } + } catch (AphrontQueryDuplicateKeyException $ex) { // We already validated the slug, but might race. Try again to see if // that's the issue. If it is, we'll throw a more specific exception. If @@ -100,4 +119,97 @@ final class PhabricatorProjectEditor { } } + private function setTransactionOldValue( + PhabricatorProject $project, + PhabricatorProjectTransaction $xaction) { + + $type = $xaction->getTransactionType(); + switch ($type) { + case PhabricatorProjectTransactionType::TYPE_NAME: + $xaction->setOldValue($project->getName()); + break; + case PhabricatorProjectTransactionType::TYPE_MEMBERS: + $affils = $project->loadAffiliations(); + $project->attachAffiliations($affils); + + $old_value = mpull($affils, 'getUserPHID'); + $old_value = array_values($old_value); + $xaction->setOldValue($affils); + + $new_value = $xaction->getNewValue(); + $new_value = array_filter($new_value); + $new_value = array_unique($new_value); + $new_value = array_values($new_value); + $xaction->setNewValue($new_value); + break; + default: + throw new Exception("Unknown transaction type '{$type}'!"); + } + } + + private function applyTransactionEffect( + PhabricatorProject $project, + PhabricatorProjectTransaction $xaction) { + + $type = $xaction->getTransactionType(); + switch ($type) { + case PhabricatorProjectTransactionType::TYPE_NAME: + $project->setName($xaction->getNewValue()); + $project->setPhrictionSlug($xaction->getNewValue()); + $this->validateName($project); + break; + case PhabricatorProjectTransactionType::TYPE_MEMBERS: + $old = array_fill_keys($xaction->getOldValue(), true); + $new = array_fill_keys($xaction->getNewValue(), true); + + $add = array(); + $rem = array(); + + foreach ($project->getAffiliations() as $affil) { + if (empty($new[$affil->getUserPHID()])) { + $rem[] = $affil; + } + } + + foreach ($new as $phid => $ignored) { + if (empty($old[$phid])) { + $affil = new PhabricatorProjectAffiliation(); + $affil->setUserPHID($phid); + $add[] = $affil; + } + } + + $this->addAffiliations = $add; + $this->remAffiliations = $rem; + break; + default: + throw new Exception("Unknown transaction type '{$type}'!"); + } + } + + private function publishTransactionStory( + PhabricatorProject $project, + PhabricatorProjectTransaction $xaction) { + + $related_phids = array( + $project->getPHID(), + $xaction->getAuthorPHID(), + ); + + id(new PhabricatorFeedStoryPublisher()) + ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_PROJECT) + ->setStoryData( + array( + 'projectPHID' => $project->getPHID(), + 'transactionID' => $xaction->getID(), + 'type' => $xaction->getTransactionType(), + 'old' => $xaction->getOldValue(), + 'new' => $xaction->getNewValue(), + )) + ->setStoryTime(time()) + ->setStoryAuthorPHID($xaction->getAuthorPHID()) + ->setRelatedPHIDs($related_phids) + ->publish(); + } + } diff --git a/src/applications/project/editor/project/__init__.php b/src/applications/project/editor/project/__init__.php index 03b4a3c276..062ebe358d 100644 --- a/src/applications/project/editor/project/__init__.php +++ b/src/applications/project/editor/project/__init__.php @@ -6,7 +6,11 @@ +phutil_require_module('phabricator', 'applications/feed/constants/story'); +phutil_require_module('phabricator', 'applications/feed/publisher'); +phutil_require_module('phabricator', 'applications/project/constants/transaction'); phutil_require_module('phabricator', 'applications/project/exception/namecollison'); +phutil_require_module('phabricator', 'applications/project/storage/affiliation'); phutil_require_module('phabricator', 'applications/project/storage/project'); phutil_require_module('phutil', 'utils'); diff --git a/src/applications/project/storage/transaction/PhabricatorProjectTransaction.php b/src/applications/project/storage/transaction/PhabricatorProjectTransaction.php new file mode 100644 index 0000000000..fad1338d66 --- /dev/null +++ b/src/applications/project/storage/transaction/PhabricatorProjectTransaction.php @@ -0,0 +1,39 @@ + array( + 'oldValue' => self::SERIALIZATION_JSON, + 'newValue' => self::SERIALIZATION_JSON, + ), + ) + parent::getConfiguration(); + } + +} diff --git a/src/applications/project/storage/transaction/__init__.php b/src/applications/project/storage/transaction/__init__.php new file mode 100644 index 0000000000..6207a6fa8d --- /dev/null +++ b/src/applications/project/storage/transaction/__init__.php @@ -0,0 +1,12 @@ +