1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-09-19 16:58:48 +02:00

Scaffolding for Fund

Summary:
Ref T5835. This is all pretty boilerplate, and does not interact with Phortune at all yet.

You can create "Initiatives", which have a title and description, and support most of the expected infrastructure (policies, transactions, mentions, edges, appsearch, remakrup, etc).

Only notable decisions:

  - Initiatives have an explicit owner. I think it's good to have a single clearly-responsible user behind an initiative.
  - I think that's it?

Test Plan:
  - Created an initiative.
  - Edited an initiative.
  - Changed application policy defaults.
  - Searched for initiatives.
  - Subscribed to an initiative.
  - Opened/closed an initiative.
  - Used `I123` and `{I123}` in remarkup.
  - Destroyed an initiative.

Reviewers: chad, btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T5835

Differential Revision: https://secure.phabricator.com/D10481
This commit is contained in:
epriestley 2014-09-11 13:38:58 -07:00
parent cae59d8345
commit e4f399b9fa
28 changed files with 1722 additions and 0 deletions

View file

@ -0,0 +1,15 @@
CREATE TABLE {$NAMESPACE}_fund.fund_initiative (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARCHAR(64) NOT NULL COLLATE utf8_bin,
name VARCHAR(255) NOT NULL,
ownerPHID VARCHAR(64) NOT NULL COLLATE utf8_bin,
description LONGTEXT NOT NULL,
viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin,
editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin,
status VARCHAR(32) NOT NULL COLLATE utf8_bin,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (phid),
KEY `key_status` (status),
KEY `key_owner` (ownerPHID)
) ENGINE=InnoDB, COLLATE utf8_general_ci;

View file

@ -0,0 +1,19 @@
CREATE TABLE {$NAMESPACE}_fund.fund_initiativetransaction (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARCHAR(64) COLLATE utf8_bin NOT NULL,
authorPHID VARCHAR(64) COLLATE utf8_bin NOT NULL,
objectPHID VARCHAR(64) COLLATE utf8_bin NOT NULL,
viewPolicy VARCHAR(64) COLLATE utf8_bin NOT NULL,
editPolicy VARCHAR(64) COLLATE utf8_bin NOT NULL,
commentPHID VARCHAR(64) COLLATE utf8_bin DEFAULT NULL,
commentVersion INT UNSIGNED NOT NULL,
transactionType VARCHAR(32) COLLATE utf8_bin NOT NULL,
oldValue LONGTEXT COLLATE utf8_bin NOT NULL,
newValue LONGTEXT COLLATE utf8_bin NOT NULL,
contentSource LONGTEXT COLLATE utf8_bin NOT NULL,
metadata LONGTEXT COLLATE utf8_bin NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (`phid`),
KEY `key_object` (`objectPHID`)
) ENGINE=InnoDB, COLLATE utf8_general_ci;

View file

@ -0,0 +1,15 @@
CREATE TABLE {$NAMESPACE}_fund.edge (
src VARCHAR(64) NOT NULL COLLATE utf8_bin,
type VARCHAR(64) NOT NULL COLLATE utf8_bin,
dst VARCHAR(64) NOT NULL COLLATE utf8_bin,
dateCreated INT UNSIGNED NOT NULL,
seq INT UNSIGNED NOT NULL,
dataID INT UNSIGNED,
PRIMARY KEY (src, type, dst),
KEY (src, type, dateCreated, seq)
) ENGINE=InnoDB, COLLATE utf8_general_ci;
CREATE TABLE {$NAMESPACE}_fund.edgedata (
id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
data LONGTEXT NOT NULL COLLATE utf8_bin
) ENGINE=InnoDB, COLLATE utf8_general_ci;

View file

@ -654,6 +654,28 @@ phutil_register_library_map(array(
'FlagDeleteConduitAPIMethod' => 'applications/flag/conduit/FlagDeleteConduitAPIMethod.php',
'FlagEditConduitAPIMethod' => 'applications/flag/conduit/FlagEditConduitAPIMethod.php',
'FlagQueryConduitAPIMethod' => 'applications/flag/conduit/FlagQueryConduitAPIMethod.php',
'FundBacking' => 'applications/fund/storage/FundBacking.php',
'FundBackingEditor' => 'applications/fund/editor/FundBackingEditor.php',
'FundBackingPHIDType' => 'applications/fund/phid/FundBackingPHIDType.php',
'FundBackingQuery' => 'applications/fund/query/FundBackingQuery.php',
'FundBackingTransaction' => 'applications/fund/storage/FundBackingTransaction.php',
'FundBackingTransactionQuery' => 'applications/fund/query/FundBackingTransactionQuery.php',
'FundController' => 'applications/fund/controller/FundController.php',
'FundCreateInitiativesCapability' => 'applications/fund/capability/FundCreateInitiativesCapability.php',
'FundDAO' => 'applications/fund/storage/FundDAO.php',
'FundDefaultViewCapability' => 'applications/fund/capability/FundDefaultViewCapability.php',
'FundInitiative' => 'applications/fund/storage/FundInitiative.php',
'FundInitiativeCloseController' => 'applications/fund/controller/FundInitiativeCloseController.php',
'FundInitiativeEditController' => 'applications/fund/controller/FundInitiativeEditController.php',
'FundInitiativeEditor' => 'applications/fund/editor/FundInitiativeEditor.php',
'FundInitiativeListController' => 'applications/fund/controller/FundInitiativeListController.php',
'FundInitiativePHIDType' => 'applications/fund/phid/FundInitiativePHIDType.php',
'FundInitiativeQuery' => 'applications/fund/query/FundInitiativeQuery.php',
'FundInitiativeRemarkupRule' => 'applications/fund/remarkup/FundInitiativeRemarkupRule.php',
'FundInitiativeSearchEngine' => 'applications/fund/query/FundInitiativeSearchEngine.php',
'FundInitiativeTransaction' => 'applications/fund/storage/FundInitiativeTransaction.php',
'FundInitiativeTransactionQuery' => 'applications/fund/query/FundInitiativeTransactionQuery.php',
'FundInitiativeViewController' => 'applications/fund/controller/FundInitiativeViewController.php',
'HarbormasterBuild' => 'applications/harbormaster/storage/build/HarbormasterBuild.php',
'HarbormasterBuildAbortedException' => 'applications/harbormaster/exception/HarbormasterBuildAbortedException.php',
'HarbormasterBuildActionController' => 'applications/harbormaster/controller/HarbormasterBuildActionController.php',
@ -1600,6 +1622,7 @@ phutil_register_library_map(array(
'PhabricatorFlagsApplication' => 'applications/flag/application/PhabricatorFlagsApplication.php',
'PhabricatorFlagsUIEventListener' => 'applications/flag/events/PhabricatorFlagsUIEventListener.php',
'PhabricatorFormExample' => 'applications/uiexample/examples/PhabricatorFormExample.php',
'PhabricatorFundApplication' => 'applications/fund/application/PhabricatorFundApplication.php',
'PhabricatorGarbageCollector' => 'infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php',
'PhabricatorGarbageCollectorConfigOptions' => 'applications/config/option/PhabricatorGarbageCollectorConfigOptions.php',
'PhabricatorGarbageCollectorDaemon' => 'infrastructure/daemon/garbagecollector/PhabricatorGarbageCollectorDaemon.php',
@ -3422,6 +3445,42 @@ phutil_register_library_map(array(
'FlagDeleteConduitAPIMethod' => 'FlagConduitAPIMethod',
'FlagEditConduitAPIMethod' => 'FlagConduitAPIMethod',
'FlagQueryConduitAPIMethod' => 'FlagConduitAPIMethod',
'FundBacking' => array(
'FundDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'FundBackingEditor' => 'PhabricatorApplicationTransactionEditor',
'FundBackingPHIDType' => 'PhabricatorPHIDType',
'FundBackingQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'FundBackingTransaction' => 'PhabricatorApplicationTransaction',
'FundBackingTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'FundController' => 'PhabricatorController',
'FundCreateInitiativesCapability' => 'PhabricatorPolicyCapability',
'FundDAO' => 'PhabricatorLiskDAO',
'FundDefaultViewCapability' => 'PhabricatorPolicyCapability',
'FundInitiative' => array(
'FundDAO',
'PhabricatorPolicyInterface',
'PhabricatorProjectInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorMentionableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorDestructibleInterface',
),
'FundInitiativeCloseController' => 'FundController',
'FundInitiativeEditController' => 'FundController',
'FundInitiativeEditor' => 'PhabricatorApplicationTransactionEditor',
'FundInitiativeListController' => 'FundController',
'FundInitiativePHIDType' => 'PhabricatorPHIDType',
'FundInitiativeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'FundInitiativeRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'FundInitiativeSearchEngine' => 'PhabricatorApplicationSearchEngine',
'FundInitiativeTransaction' => 'PhabricatorApplicationTransaction',
'FundInitiativeTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'FundInitiativeViewController' => 'FundController',
'HarbormasterBuild' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
@ -4465,6 +4524,7 @@ phutil_register_library_map(array(
'PhabricatorFlagsApplication' => 'PhabricatorApplication',
'PhabricatorFlagsUIEventListener' => 'PhabricatorEventListener',
'PhabricatorFormExample' => 'PhabricatorUIExample',
'PhabricatorFundApplication' => 'PhabricatorApplication',
'PhabricatorGarbageCollector' => 'Phobject',
'PhabricatorGarbageCollectorConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorGarbageCollectorDaemon' => 'PhabricatorDaemon',

View file

@ -0,0 +1,62 @@
<?php
final class PhabricatorFundApplication extends PhabricatorApplication {
public function getName() {
return pht('Fund');
}
public function getBaseURI() {
return '/fund/';
}
public function getShortDescription() {
return pht('Donate');
}
public function getIconName() {
return 'phund';
}
public function getTitleGlyph() {
return "\xE2\x99\xA5";
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function isBeta() {
return true;
}
public function getRemarkupRules() {
return array(
new FundInitiativeRemarkupRule(),
);
}
public function getRoutes() {
return array(
'/I(?P<id>[1-9]\d*)' => 'FundInitiativeViewController',
'/fund/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' => 'FundInitiativeListController',
'create/' => 'FundInitiativeEditController',
'edit/(?:(?P<id>[^/]+)/)?' => 'FundInitiativeEditController',
'close/(?P<id>[^/]+)/' => 'FundInitiativeCloseController',
),
);
}
protected function getCustomCapabilities() {
return array(
FundDefaultViewCapability::CAPABILITY => array(
'caption' => pht('Default view policy for newly created initiatives.'),
),
FundCreateInitiativesCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
);
}
}

View file

@ -0,0 +1,16 @@
<?php
final class FundCreateInitiativesCapability
extends PhabricatorPolicyCapability {
const CAPABILITY = 'fund.create';
public function getCapabilityName() {
return pht('Can Create Initiatives');
}
public function describeCapabilityRejection() {
return pht('You do not have permission to create Fund initiatives.');
}
}

View file

@ -0,0 +1,15 @@
<?php
final class FundDefaultViewCapability extends PhabricatorPolicyCapability {
const CAPABILITY = 'fund.default.view';
public function getCapabilityName() {
return pht('Default View Policy');
}
public function shouldAllowPublicPolicySetting() {
return true;
}
}

View file

@ -0,0 +1,3 @@
<?php
abstract class FundController extends PhabricatorController {}

View file

@ -0,0 +1,73 @@
<?php
final class FundInitiativeCloseController
extends FundController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$initiative = id(new FundInitiativeQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$initiative) {
return new Aphront404Response();
}
$initiative_uri = '/'.$initiative->getMonogram();
$is_close = !$initiative->isClosed();
if ($request->isFormPost()) {
$type_status = FundInitiativeTransaction::TYPE_STATUS;
if ($is_close) {
$new_status = FundInitiative::STATUS_CLOSED;
} else {
$new_status = FundInitiative::STATUS_OPEN;
}
$xaction = id(new FundInitiativeTransaction())
->setTransactionType($type_status)
->setNewValue($new_status);
$editor = id(new FundInitiativeEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true);
$editor->applyTransactions($initiative, array($xaction));
return id(new AphrontRedirectResponse())->setURI($initiative_uri);
}
if ($is_close) {
$title = pht('Close Initiative?');
$body = pht('Really close this initiative?');
$button_text = pht('Close Initiative');
} else {
$title = pht('Reopen Initiative?');
$body = pht('Really reopen this initiative?');
$button_text = pht('Reopen Initiative');
}
return $this->newDialog()
->setTitle($title)
->appendParagraph($body)
->addCancelButton($initiative_uri)
->addSubmitButton($button_text);
}
}

View file

@ -0,0 +1,191 @@
<?php
final class FundInitiativeEditController
extends FundController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
if ($this->id) {
$initiative = id(new FundInitiativeQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$initiative) {
return new Aphront404Response();
}
$is_new = false;
} else {
$initiative = FundInitiative::initializeNewInitiative($viewer);
$is_new = true;
}
if ($is_new) {
$title = pht('Create Initiative');
$button_text = pht('Create Initiative');
$cancel_uri = $this->getApplicationURI();
} else {
$title = pht(
'Edit %s %s',
$initiative->getMonogram(),
$initiative->getName());
$button_text = pht('Save Changes');
$cancel_uri = '/'.$initiative->getMonogram();
}
$e_name = true;
$v_name = $initiative->getName();
$v_desc = $initiative->getDescription();
if ($is_new) {
$v_projects = array();
} else {
$v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs(
$initiative->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
$v_projects = array_reverse($v_projects);
}
$validation_exception = null;
if ($request->isFormPost()) {
$v_name = $request->getStr('name');
$v_desc = $request->getStr('description');
$v_view = $request->getStr('viewPolicy');
$v_edit = $request->getStr('editPolicy');
$v_projects = $request->getArr('projects');
$type_name = FundInitiativeTransaction::TYPE_NAME;
$type_desc = FundInitiativeTransaction::TYPE_DESCRIPTION;
$type_view = PhabricatorTransactions::TYPE_VIEW_POLICY;
$type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY;
$xactions = array();
$xactions[] = id(new FundInitiativeTransaction())
->setTransactionType($type_name)
->setNewValue($v_name);
$xactions[] = id(new FundInitiativeTransaction())
->setTransactionType($type_desc)
->setNewValue($v_desc);
$xactions[] = id(new FundInitiativeTransaction())
->setTransactionType($type_view)
->setNewValue($v_view);
$xactions[] = id(new FundInitiativeTransaction())
->setTransactionType($type_edit)
->setNewValue($v_edit);
$proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$xactions[] = id(new FundInitiativeTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $proj_edge_type)
->setNewValue(array('=' => array_fuse($v_projects)));
$editor = id(new FundInitiativeEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
try {
$editor->applyTransactions($initiative, $xactions);
return id(new AphrontRedirectResponse())
->setURI('/'.$initiative->getMonogram());
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$e_name = $ex->getShortMessage($type_name);
$initiative->setViewPolicy($v_view);
$initiative->setEditPolicy($v_edit);
}
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($initiative)
->execute();
if ($v_projects) {
$project_handles = $this->loadViewerHandles($v_projects);
} else {
$project_handles = array();
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setLabel(pht('Name'))
->setValue($v_name)
->setError($e_name))
->appendChild(
id(new PhabricatorRemarkupControl())
->setName('description')
->setLabel(pht('Description'))
->setValue($v_desc))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects'))
->setName('projects')
->setValue($project_handles)
->setDatasource(new PhabricatorProjectDatasource()))
->appendChild(
id(new AphrontFormPolicyControl())
->setName('viewPolicy')
->setPolicyObject($initiative)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicies($policies))
->appendChild(
id(new AphrontFormPolicyControl())
->setName('editPolicy')
->setPolicyObject($initiative)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicies($policies))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($button_text)
->addCancelButton($cancel_uri));
$crumbs = $this->buildApplicationCrumbs();
if ($is_new) {
$crumbs->addTextCrumb(pht('Create Initiative'));
} else {
$crumbs->addTextCrumb(
$initiative->getMonogram(),
'/'.$initiative->getMonogram());
$crumbs->addTextCrumb(pht('Edit'));
}
$box = id(new PHUIObjectBoxView())
->setValidationException($validation_exception)
->setHeaderText($title)
->appendChild($form);
return $this->buildApplicationPage(
array(
$crumbs,
$box,
),
array(
'title' => $title,
));
}
}

View file

@ -0,0 +1,53 @@
<?php
final class FundInitiativeListController
extends FundController {
private $queryKey;
public function willProcessRequest(array $data) {
$this->queryKey = idx($data, 'queryKey');
}
public function processRequest() {
$request = $this->getRequest();
$controller = id(new PhabricatorApplicationSearchController($request))
->setQueryKey($this->queryKey)
->setSearchEngine(new FundInitiativeSearchEngine())
->setNavigation($this->buildSideNavView());
return $this->delegateToController($controller);
}
public function buildSideNavView() {
$user = $this->getRequest()->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
id(new FundInitiativeSearchEngine())
->setViewer($user)
->addNavigationItems($nav->getMenu());
$nav->selectFilter(null);
return $nav;
}
public function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
$can_create = $this->hasApplicationCapability(
FundCreateInitiativesCapability::CAPABILITY);
$crumbs->addAction(
id(new PHUIListItemView())
->setName(pht('Create Initiative'))
->setHref($this->getApplicationURI('create/'))
->setIcon('fa-plus-square')
->setDisabled(!$can_create)
->setWorkflow(!$can_create));
return $crumbs;
}
}

View file

@ -0,0 +1,149 @@
<?php
final class FundInitiativeViewController
extends FundController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$initiative = id(new FundInitiativeQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->executeOne();
if (!$initiative) {
return new Aphront404Response();
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($initiative->getMonogram());
$title = pht(
'%s %s',
$initiative->getMonogram(),
$initiative->getName());
if ($initiative->isClosed()) {
$status_icon = 'fa-times';
$status_color = 'bluegrey';
} else {
$status_icon = 'fa-check';
$status_color = 'bluegrey';
}
$status_name = idx(
FundInitiative::getStatusNameMap(),
$initiative->getStatus());
$header = id(new PHUIHeaderView())
->setObjectName($initiative->getMonogram())
->setHeader($initiative->getName())
->setUser($viewer)
->setPolicyObject($initiative)
->setStatus($status_icon, $status_color, $status_name);
$properties = $this->buildPropertyListView($initiative);
$actions = $this->buildActionListView($initiative);
$properties->setActionList($actions);
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($properties);
$xactions = id(new FundInitiativeTransactionQuery())
->setViewer($viewer)
->withObjectPHIDs(array($initiative->getPHID()))
->execute();
$timeline = id(new PhabricatorApplicationTransactionView())
->setUser($viewer)
->setObjectPHID($initiative->getPHID())
->setTransactions($xactions);
return $this->buildApplicationPage(
array(
$crumbs,
$box,
$timeline,
),
array(
'title' => $title,
));
}
private function buildPropertyListView(FundInitiative $initiative) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($initiative);
$owner_phid = $initiative->getOwnerPHID();
$this->loadHandles(array($owner_phid));
$view->addProperty(
pht('Owner'),
$this->getHandle($owner_phid)->renderLink());
$view->invokeWillRenderEvent();
$description = $initiative->getDescription();
if (strlen($description)) {
$description = PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())->setContent($description),
'default',
$viewer);
$view->addSectionHeader(pht('Description'));
$view->addTextContent($description);
}
return $view;
}
private function buildActionListView(FundInitiative $initiative) {
$viewer = $this->getRequest()->getUser();
$id = $initiative->getID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$initiative,
PhabricatorPolicyCapability::CAN_EDIT);
$view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($initiative);
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Initiative'))
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref($this->getApplicationURI("/edit/{$id}/")));
if ($initiative->isClosed()) {
$close_name = pht('Reopen Initiative');
$close_icon = 'fa-check';
} else {
$close_name = pht('Close Initiative');
$close_icon = 'fa-times';
}
$view->addAction(
id(new PhabricatorActionView())
->setName($close_name)
->setIcon($close_icon)
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($this->getApplicationURI("/close/{$id}/")));
return $view;
}
}

View file

@ -0,0 +1,13 @@
<?php
final class FundBackingEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorFundApplication';
}
public function getEditorObjectsDescription() {
return pht('Fund Backing');
}
}

View file

@ -0,0 +1,123 @@
<?php
final class FundInitiativeEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorFundApplication';
}
public function getEditorObjectsDescription() {
return pht('Fund Initiatives');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = FundInitiativeTransaction::TYPE_NAME;
$types[] = FundInitiativeTransaction::TYPE_DESCRIPTION;
$types[] = FundInitiativeTransaction::TYPE_STATUS;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case FundInitiativeTransaction::TYPE_NAME:
return $object->getName();
case FundInitiativeTransaction::TYPE_DESCRIPTION:
return $object->getDescription();
case FundInitiativeTransaction::TYPE_STATUS:
return $object->getStatus();
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case FundInitiativeTransaction::TYPE_NAME:
case FundInitiativeTransaction::TYPE_DESCRIPTION:
case FundInitiativeTransaction::TYPE_STATUS:
return $xaction->getNewValue();
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case FundInitiativeTransaction::TYPE_NAME:
$object->setName($xaction->getNewValue());
return;
case FundInitiativeTransaction::TYPE_DESCRIPTION:
$object->setDescription($xaction->getNewValue());
return;
case FundInitiativeTransaction::TYPE_STATUS:
$object->setStatus($xaction->getNewValue());
return;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
case PhabricatorTransactions::TYPE_EDGE:
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case FundInitiativeTransaction::TYPE_NAME:
case FundInitiativeTransaction::TYPE_DESCRIPTION:
case FundInitiativeTransaction::TYPE_STATUS:
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
case PhabricatorTransactions::TYPE_EDGE:
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
switch ($type) {
case FundInitiativeTransaction::TYPE_NAME:
$missing = $this->validateIsEmptyTextField(
$object->getName(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('Initiative name is required.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
break;
}
return $errors;
}
}

View file

@ -0,0 +1,41 @@
<?php
final class FundBackingPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'FBAK';
public function getTypeName() {
return pht('Variable');
}
public function newObject() {
return new FundInitiative();
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new FundInitiativeQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$initiative = $objects[$phid];
$id = $initiative->getID();
$monogram = $initiative->getMonogram();
$name = $initiative->getName();
$handle->setName($name);
$handle->setFullName("{$monogram} {$name}");
$handle->setURI("/fund/view/{$id}/");
}
}
}

View file

@ -0,0 +1,74 @@
<?php
final class FundInitiativePHIDType extends PhabricatorPHIDType {
const TYPECONST = 'FITV';
public function getTypeName() {
return pht('Initiative');
}
public function newObject() {
return new FundInitiative();
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new FundInitiativeQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$initiative = $objects[$phid];
$id = $initiative->getID();
$monogram = $initiative->getMonogram();
$name = $initiative->getName();
if ($initiative->isClosed()) {
$handle->setStatus(PhabricatorObjectHandleStatus::STATUS_CLOSED);
}
$handle->setName($name);
$handle->setFullName("{$monogram} {$name}");
$handle->setURI("/I{$id}");
}
}
public function canLoadNamedObject($name) {
return preg_match('/^I\d*[1-9]\d*$/i', $name);
}
public function loadNamedObjects(
PhabricatorObjectQuery $query,
array $names) {
$id_map = array();
foreach ($names as $name) {
$id = (int)substr($name, 1);
$id_map[$id][] = $name;
}
$objects = id(new FundInitiativeQuery())
->setViewer($query->getViewer())
->withIDs(array_keys($id_map))
->execute();
$results = array();
foreach ($objects as $id => $object) {
foreach (idx($id_map, $id, array()) as $name) {
$results[$name] = $object;
}
}
return $results;
}
}

View file

@ -0,0 +1,60 @@
<?php
final class FundBackingQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
protected function loadPage() {
$table = new FundBacking();
$conn_r = $table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($rows);
}
private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
$where[] = $this->buildPagingClause($conn_r);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn_r,
'phid IN (%Ls)',
$this->phids);
}
return $this->formatWhereClause($where);
}
public function getQueryApplicationClass() {
return 'PhabricatorFundApplication';
}
}

View file

@ -0,0 +1,10 @@
<?php
final class FundBackingTransactionQuery
extends PhabricatorApplicationTransactionQuery {
public function getTemplateApplicationTransaction() {
return new FundBackingTransaction();
}
}

View file

@ -0,0 +1,116 @@
<?php
final class FundInitiativeQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $ownerPHIDs;
private $statuses;
private $needProjectPHIDs;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withOwnerPHIDs(array $phids) {
$this->ownerPHIDs = $phids;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function needProjectPHIDs($need) {
$this->needProjectPHIDs = $need;
return $this;
}
protected function loadPage() {
$table = new FundInitiative();
$conn_r = $table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($rows);
}
protected function didFilterPage(array $initiatives) {
if ($this->needProjectPHIDs) {
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(mpull($initiatives, 'getPHID'))
->withEdgeTypes(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
));
$edge_query->execute();
foreach ($initiatives as $initiative) {
$phids = $edge_query->getDestinationPHIDs(
array(
$initiative->getPHID(),
));
$initiative->attachProjectPHIDs($phids);
}
}
return $initiatives;
}
private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
$where[] = $this->buildPagingClause($conn_r);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn_r,
'phid IN (%Ls)',
$this->phids);
}
if ($this->ownerPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'ownerPHID IN (%Ls)',
$this->ownerPHIDs);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn_r,
'status IN (%Ls)',
$this->statuses);
}
return $this->formatWhereClause($where);
}
public function getQueryApplicationClass() {
return 'PhabricatorFundApplication';
}
}

View file

@ -0,0 +1,180 @@
<?php
final class FundInitiativeSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Fund Initiatives');
}
public function getApplicationClassName() {
return 'PhabricatorFundApplication';
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
$saved->setParameter(
'ownerPHIDs',
$this->readUsersFromRequest($request, 'owners'));
$saved->setParameter(
'statuses',
$this->readListFromRequest($request, 'statuses'));
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new FundInitiativeQuery())
->needProjectPHIDs(true);
$owner_phids = $saved->getParameter('ownerPHIDs');
if ($owner_phids) {
$query->withOwnerPHIDs($owner_phids);
}
$statuses = $saved->getParameter('statuses');
if ($statuses) {
$query->withStatuses($statuses);
}
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$statuses = $saved->getParameter('statuses', array());
$statuses = array_fuse($statuses);
$owner_phids = $saved->getParameter('ownerPHIDs', array());
$all_phids = array_mergev(
array(
$owner_phids,
));
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireViewer())
->withPHIDs($all_phids)
->execute();
$status_map = FundInitiative::getStatusNameMap();
$status_control = id(new AphrontFormCheckboxControl())
->setLabel(pht('Statuses'));
foreach ($status_map as $status => $name) {
$status_control->addCheckbox(
'statuses[]',
$status,
$name,
isset($statuses[$status]));
}
$form
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Owners'))
->setName('owners')
->setDatasource(new PhabricatorPeopleDatasource())
->setValue(array_select_keys($handles, $owner_phids)))
->appendChild($status_control);
}
protected function getURI($path) {
return '/fund/'.$path;
}
public function getBuiltinQueryNames() {
$names = array();
$names['open'] = pht('Open Initiatives');
if ($this->requireViewer()->isLoggedIn()) {
$names['owned'] = pht('Owned Initiatives');
}
$names['all'] = pht('All Initiatives');
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
case 'owned':
return $query->setParameter(
'ownerPHIDs',
array(
$this->requireViewer()->getPHID(),
));
case 'open':
return $query->setParameter(
'statuses',
array(
FundInitiative::STATUS_OPEN,
));
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function getRequiredHandlePHIDsForResultList(
array $initiatives,
PhabricatorSavedQuery $query) {
$phids = array();
foreach ($initiatives as $initiative) {
$phids[] = $initiative->getOwnerPHID();
foreach ($initiative->getProjectPHIDs() as $project_phid) {
$phids[] = $project_phid;
}
}
return $phids;
}
protected function renderResultList(
array $initiatives,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($initiatives, 'FundInitiative');
$viewer = $this->requireViewer();
$list = id(new PHUIObjectItemListView());
foreach ($initiatives as $initiative) {
$owner_handle = $handles[$initiative->getOwnerPHID()];
$item = id(new PHUIObjectItemView())
->setObjectName($initiative->getMonogram())
->setHeader($initiative->getName())
->setHref('/'.$initiative->getMonogram())
->addByline(pht('Owner: %s', $owner_handle->renderLink()));
if ($initiative->isClosed()) {
$item->setDisabled(true);
}
$project_handles = array_select_keys(
$handles,
$initiative->getProjectPHIDs());
if ($project_handles) {
$item->addAttribute(
id(new PHUIHandleTagListView())
->setLimit(4)
->setSlim(true)
->setHandles($project_handles));
}
$list->addItem($item);
}
return $list;
}
}

View file

@ -0,0 +1,10 @@
<?php
final class FundInitiativeTransactionQuery
extends PhabricatorApplicationTransactionQuery {
public function getTemplateApplicationTransaction() {
return new FundInitiativeTransaction();
}
}

View file

@ -0,0 +1,18 @@
<?php
final class FundInitiativeRemarkupRule extends PhabricatorObjectRemarkupRule {
protected function getObjectNamePrefix() {
return 'I';
}
protected function loadObjects(array $ids) {
$viewer = $this->getEngine()->getConfig('viewer');
return id(new FundInitiativeQuery())
->setViewer($viewer)
->withIDs($ids)
->execute();
}
}

View file

@ -0,0 +1,85 @@
<?php
final class FundBacking extends FundDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface {
protected $initiativePHID;
protected $backerPHID;
protected $purchasePHID;
protected $amountInCents;
protected $status;
protected $properties = array();
private $initiative = self::ATTACHABLE;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(FundBackingPHIDType::TYPECONST);
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
// If we have the initiative, use the initiative's policy.
// Otherwise, return NOONE. This allows the backer to continue seeing
// a backing even if they're no longer allowed to see the initiative.
$initiative = $this->getInitiative();
if ($initiative) {
return $initiative->getPolicy($capability);
}
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getBackerPHID());
}
public function describeAutomaticCapability($capability) {
return pht('A backer can always see what they have backed.');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new FundBackingEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new FundBackingTransaction();
}
}

View file

@ -0,0 +1,18 @@
<?php
final class FundBackingTransaction
extends PhabricatorApplicationTransaction {
public function getApplicationName() {
return 'fund';
}
public function getApplicationTransactionType() {
return FundBackingPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return null;
}
}

View file

@ -0,0 +1,9 @@
<?php
abstract class FundDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'fund';
}
}

View file

@ -0,0 +1,157 @@
<?php
final class FundInitiative extends FundDAO
implements
PhabricatorPolicyInterface,
PhabricatorProjectInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorSubscribableInterface,
PhabricatorMentionableInterface,
PhabricatorFlaggableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorDestructibleInterface {
protected $name;
protected $ownerPHID;
protected $description;
protected $viewPolicy;
protected $editPolicy;
protected $status;
private $projectPHIDs = self::ATTACHABLE;
const STATUS_OPEN = 'open';
const STATUS_CLOSED = 'closed';
public static function getStatusNameMap() {
return array(
self::STATUS_OPEN => pht('Open'),
self::STATUS_CLOSED => pht('Closed'),
);
}
public static function initializeNewInitiative(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorFundApplication'))
->executeOne();
$view_policy = $app->getPolicy(FundDefaultViewCapability::CAPABILITY);
return id(new FundInitiative())
->setOwnerPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setEditPolicy($actor->getPHID())
->setStatus(self::STATUS_OPEN);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(FundInitiativePHIDType::TYPECONST);
}
public function getMonogram() {
return 'I'.$this->getID();
}
public function getProjectPHIDs() {
return $this->assertAttached($this->projectPHIDs);
}
public function attachProjectPHIDs(array $phids) {
$this->projectPHIDs = $phids;
return $this;
}
public function isClosed() {
return ($this->getStatus() == self::STATUS_CLOSED);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new FundInitiativeEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new FundInitiativeTransaction();
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getOwnerPHID());
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorTokenRecevierInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getOwnerPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}

View file

@ -0,0 +1,136 @@
<?php
final class FundInitiativeTransaction
extends PhabricatorApplicationTransaction {
const TYPE_NAME = 'fund:name';
const TYPE_DESCRIPTION = 'fund:description';
const TYPE_STATUS = 'fund:status';
public function getApplicationName() {
return 'fund';
}
public function getApplicationTransactionType() {
return FundInitiativePHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return null;
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
$type = $this->getTransactionType();
switch ($type) {
case FundInitiativeTransaction::TYPE_NAME:
if ($old === null) {
return pht(
'%s created this initiative.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s renamed this initiative from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old,
$new);
}
break;
case FundInitiativeTransaction::TYPE_DESCRIPTION:
return pht(
'%s edited the description of this initiative.',
$this->renderHandleLink($author_phid));
case FundInitiativeTransaction::TYPE_STATUS:
switch ($new) {
case FundInitiative::STATUS_OPEN:
return pht(
'%s reopened this initiative.',
$this->renderHandleLink($author_phid));
case FundInitiative::STATUS_CLOSED:
return pht(
'%s closed this initiative.',
$this->renderHandleLink($author_phid));
}
break;
}
return parent::getTitle();
}
public function getTitleForFeed(PhabricatorFeedStory $story) {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
$type = $this->getTransactionType();
switch ($type) {
case FundInitiativeTransaction::TYPE_NAME:
if ($old === null) {
return pht(
'%s created %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
} else {
return pht(
'%s renamed %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
}
break;
case FundInitiativeTransaction::TYPE_DESCRIPTION:
return pht(
'%s updated the description for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case FundInitiativeTransaction::TYPE_STATUS:
switch ($new) {
case FundInitiative::STATUS_OPEN:
return pht(
'%s reopened %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case FundInitiative::STATUS_CLOSED:
return pht(
'%s closed %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
}
break;
}
return parent::getTitleForFeed($story);
}
public function shouldHide() {
$old = $this->getOldValue();
switch ($this->getTransactionType()) {
case FundInitiativeTransaction::TYPE_DESCRIPTION:
return ($old === null);
}
return parent::shouldHide();
}
public function hasChangeDetails() {
switch ($this->getTransactionType()) {
case FundInitiativeTransaction::TYPE_DESCRIPTION:
return ($this->getOldValue() !== null);
}
return parent::hasChangeDetails();
}
public function renderChangeDetails(PhabricatorUser $viewer) {
return $this->renderTextCorpusChangeDetails(
$viewer,
$this->getOldValue(),
$this->getNewValue());
}
}

View file

@ -119,6 +119,7 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'db.phragment' => array(),
'db.dashboard' => array(),
'db.system' => array(),
'db.fund' => array(),
'0000.legacy.sql' => array(
'legacy' => 0,
),