Page MenuHomePhabricator

D10481.diff
No OneTemporary

D10481.diff

diff --git a/resources/sql/autopatches/20140911.fund.1.initiative.sql b/resources/sql/autopatches/20140911.fund.1.initiative.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20140911.fund.1.initiative.sql
@@ -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;
diff --git a/resources/sql/autopatches/20140911.fund.2.xaction.sql b/resources/sql/autopatches/20140911.fund.2.xaction.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20140911.fund.2.xaction.sql
@@ -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;
diff --git a/resources/sql/autopatches/20140911.fund.3.edge.sql b/resources/sql/autopatches/20140911.fund.3.edge.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20140911.fund.3.edge.sql
@@ -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;
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -654,6 +654,28 @@
'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 @@
'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 @@
'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 @@
'PhabricatorFlagsApplication' => 'PhabricatorApplication',
'PhabricatorFlagsUIEventListener' => 'PhabricatorEventListener',
'PhabricatorFormExample' => 'PhabricatorUIExample',
+ 'PhabricatorFundApplication' => 'PhabricatorApplication',
'PhabricatorGarbageCollector' => 'Phobject',
'PhabricatorGarbageCollectorConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorGarbageCollectorDaemon' => 'PhabricatorDaemon',
diff --git a/src/applications/fund/application/PhabricatorFundApplication.php b/src/applications/fund/application/PhabricatorFundApplication.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/application/PhabricatorFundApplication.php
@@ -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,
+ ),
+ );
+ }
+
+}
diff --git a/src/applications/fund/capability/FundCreateInitiativesCapability.php b/src/applications/fund/capability/FundCreateInitiativesCapability.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/capability/FundCreateInitiativesCapability.php
@@ -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.');
+ }
+
+}
diff --git a/src/applications/fund/capability/FundDefaultViewCapability.php b/src/applications/fund/capability/FundDefaultViewCapability.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/capability/FundDefaultViewCapability.php
@@ -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;
+ }
+
+}
diff --git a/src/applications/fund/controller/FundController.php b/src/applications/fund/controller/FundController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/controller/FundController.php
@@ -0,0 +1,3 @@
+<?php
+
+abstract class FundController extends PhabricatorController {}
diff --git a/src/applications/fund/controller/FundInitiativeCloseController.php b/src/applications/fund/controller/FundInitiativeCloseController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/controller/FundInitiativeCloseController.php
@@ -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);
+ }
+
+}
diff --git a/src/applications/fund/controller/FundInitiativeEditController.php b/src/applications/fund/controller/FundInitiativeEditController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/controller/FundInitiativeEditController.php
@@ -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,
+ ));
+ }
+
+}
diff --git a/src/applications/fund/controller/FundInitiativeListController.php b/src/applications/fund/controller/FundInitiativeListController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/controller/FundInitiativeListController.php
@@ -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;
+ }
+
+}
diff --git a/src/applications/fund/controller/FundInitiativeViewController.php b/src/applications/fund/controller/FundInitiativeViewController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/controller/FundInitiativeViewController.php
@@ -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;
+ }
+
+}
diff --git a/src/applications/fund/editor/FundBackingEditor.php b/src/applications/fund/editor/FundBackingEditor.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/editor/FundBackingEditor.php
@@ -0,0 +1,13 @@
+<?php
+
+final class FundBackingEditor
+ extends PhabricatorApplicationTransactionEditor {
+
+ public function getEditorApplicationClass() {
+ return 'PhabricatorFundApplication';
+ }
+
+ public function getEditorObjectsDescription() {
+ return pht('Fund Backing');
+ }
+}
diff --git a/src/applications/fund/editor/FundInitiativeEditor.php b/src/applications/fund/editor/FundInitiativeEditor.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/editor/FundInitiativeEditor.php
@@ -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;
+ }
+
+
+}
diff --git a/src/applications/fund/phid/FundBackingPHIDType.php b/src/applications/fund/phid/FundBackingPHIDType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/phid/FundBackingPHIDType.php
@@ -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}/");
+ }
+ }
+
+}
diff --git a/src/applications/fund/phid/FundInitiativePHIDType.php b/src/applications/fund/phid/FundInitiativePHIDType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/phid/FundInitiativePHIDType.php
@@ -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;
+ }
+
+}
diff --git a/src/applications/fund/query/FundBackingQuery.php b/src/applications/fund/query/FundBackingQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/query/FundBackingQuery.php
@@ -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';
+ }
+
+}
diff --git a/src/applications/fund/query/FundBackingTransactionQuery.php b/src/applications/fund/query/FundBackingTransactionQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/query/FundBackingTransactionQuery.php
@@ -0,0 +1,10 @@
+<?php
+
+final class FundBackingTransactionQuery
+ extends PhabricatorApplicationTransactionQuery {
+
+ public function getTemplateApplicationTransaction() {
+ return new FundBackingTransaction();
+ }
+
+}
diff --git a/src/applications/fund/query/FundInitiativeQuery.php b/src/applications/fund/query/FundInitiativeQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/query/FundInitiativeQuery.php
@@ -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';
+ }
+
+}
diff --git a/src/applications/fund/query/FundInitiativeSearchEngine.php b/src/applications/fund/query/FundInitiativeSearchEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/query/FundInitiativeSearchEngine.php
@@ -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;
+ }
+
+}
diff --git a/src/applications/fund/query/FundInitiativeTransactionQuery.php b/src/applications/fund/query/FundInitiativeTransactionQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/query/FundInitiativeTransactionQuery.php
@@ -0,0 +1,10 @@
+<?php
+
+final class FundInitiativeTransactionQuery
+ extends PhabricatorApplicationTransactionQuery {
+
+ public function getTemplateApplicationTransaction() {
+ return new FundInitiativeTransaction();
+ }
+
+}
diff --git a/src/applications/fund/remarkup/FundInitiativeRemarkupRule.php b/src/applications/fund/remarkup/FundInitiativeRemarkupRule.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/remarkup/FundInitiativeRemarkupRule.php
@@ -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();
+ }
+
+}
diff --git a/src/applications/fund/storage/FundBacking.php b/src/applications/fund/storage/FundBacking.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/storage/FundBacking.php
@@ -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();
+ }
+
+}
diff --git a/src/applications/fund/storage/FundBackingTransaction.php b/src/applications/fund/storage/FundBackingTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/storage/FundBackingTransaction.php
@@ -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;
+ }
+
+}
diff --git a/src/applications/fund/storage/FundDAO.php b/src/applications/fund/storage/FundDAO.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/storage/FundDAO.php
@@ -0,0 +1,9 @@
+<?php
+
+abstract class FundDAO extends PhabricatorLiskDAO {
+
+ public function getApplicationName() {
+ return 'fund';
+ }
+
+}
diff --git a/src/applications/fund/storage/FundInitiative.php b/src/applications/fund/storage/FundInitiative.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/storage/FundInitiative.php
@@ -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();
+ }
+
+}
diff --git a/src/applications/fund/storage/FundInitiativeTransaction.php b/src/applications/fund/storage/FundInitiativeTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fund/storage/FundInitiativeTransaction.php
@@ -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());
+ }
+}
diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
--- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
+++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
@@ -119,6 +119,7 @@
'db.phragment' => array(),
'db.dashboard' => array(),
'db.system' => array(),
+ 'db.fund' => array(),
'0000.legacy.sql' => array(
'legacy' => 0,
),

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 18, 9:44 AM (6 d, 11 h ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/q5/wg/grbvpmrmehuw7ssn
Default Alt Text
D10481.diff (56 KB)

Event Timeline