1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-18 19:40:55 +01:00

Add transaction-oriented editing to projects

Summary:
  - Make some editing operations transaction-oriented, like Maniphest. (This
seems to be a good model, particularly for extensibility.) I'll move the rest of
the editing operations to transactions in future diffs.
  - Make transaction-oriented operations publish feed stories.

Test Plan:
  - Created a new project.
  - Edited an existing project.
  - Created a new project via quick create flow from Maniphest.
  - Verified feed stories publish correctly.

Reviewers: btrahan, jungejason

Reviewed By: btrahan

CC: aran, epriestley

Maniphest Tasks: T681

Differential Revision: https://secure.phabricator.com/D1477
This commit is contained in:
epriestley 2012-01-24 07:11:37 -08:00
parent 3c8bb8a608
commit b43eb5aa7c
18 changed files with 383 additions and 26 deletions

View file

@ -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;

View file

@ -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',

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,10 +19,11 @@
final class PhabricatorFeedStoryTypeConstants
extends PhabricatorFeedConstants {
const STORY_UNKNOWN = 'PhabricatorFeedStoryUnknown';
const STORY_STATUS = 'PhabricatorFeedStoryStatus';
const STORY_DIFFERENTIAL = 'PhabricatorFeedStoryDifferential';
const STORY_PHRICTION = 'PhabricatorFeedStoryPhriction';
const STORY_MANIPHEST = 'PhabricatorFeedStoryManiphest';
const STORY_UNKNOWN = 'PhabricatorFeedStoryUnknown';
const STORY_STATUS = 'PhabricatorFeedStoryStatus';
const STORY_DIFFERENTIAL = 'PhabricatorFeedStoryDifferential';
const STORY_PHRICTION = 'PhabricatorFeedStoryPhriction';
const STORY_MANIPHEST = 'PhabricatorFeedStoryManiphest';
const STORY_PROJECT = 'PhabricatorFeedStoryProject';
}

View file

@ -0,0 +1,71 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class PhabricatorFeedStoryProject extends PhabricatorFeedStory {
public function getRequiredHandlePHIDs() {
return array(
$this->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 '.
'<strong>'.$proj->renderLink().'</strong>'.
' from '.
'<strong>'.phutil_escape_html($old).'</strong>'.
' to '.
'<strong>'.phutil_escape_html($new).'</strong>.';
} else {
$action = 'created project '.
'<strong>'.$proj->renderLink().'</strong>'.
' (as '.
'<strong>'.phutil_escape_html($new).'</strong>).';
}
break;
default:
$action = 'updated project <strong>'.$proj->renderLink().'</strong>';
break;
}
$view->setTitle('<strong>'.$auth->renderLink().'</strong> '.$action);
$view->setOneLineStory(true);
return $view;
}
}

View file

@ -0,0 +1,16 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/feed/story/base');
phutil_require_module('phabricator', 'applications/feed/view/story');
phutil_require_module('phabricator', 'applications/project/constants/transaction');
phutil_require_module('phutil', 'markup');
phutil_require_source('PhabricatorFeedStoryProject.php');

View file

@ -0,0 +1,21 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class PhabricatorProjectConstants {
}

View file

@ -0,0 +1,10 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_source('PhabricatorProjectConstants.php');

View file

@ -0,0 +1,25 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorProjectTransactionType
extends PhabricatorProjectConstants {
const TYPE_NAME = 'name';
const TYPE_MEMBERS = 'members';
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/project/constants/base');
phutil_require_source('PhabricatorProjectTransactionType.php');

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -34,10 +34,16 @@ class PhabricatorProjectCreateController
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();

View file

@ -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');

View file

@ -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();

View file

@ -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');

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,45 +20,64 @@ final class PhabricatorProjectEditor {
private $project;
private $user;
private $projectName;
private $addAffiliations;
private $remAffiliations;
public function __construct(PhabricatorProject $project) {
$this->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();
}
}

View file

@ -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');

View file

@ -0,0 +1,39 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group project
*/
class PhabricatorProjectTransaction extends PhabricatorProjectDAO {
protected $projectID;
protected $authorPHID;
protected $transactionType;
protected $oldValue;
protected $newValue;
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'oldValue' => self::SERIALIZATION_JSON,
'newValue' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/project/storage/base');
phutil_require_source('PhabricatorProjectTransaction.php');

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.