diff --git a/resources/sql/autopatches/20141007.fundrisks.sql b/resources/sql/autopatches/20141007.fundrisks.sql new file mode 100644 index 000000000..3fbbd0816 --- /dev/null +++ b/resources/sql/autopatches/20141007.fundrisks.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_fund.fund_initiative + ADD risks LONGTEXT NOT NULL COLLATE utf8_bin; diff --git a/src/applications/fund/controller/FundInitiativeBackController.php b/src/applications/fund/controller/FundInitiativeBackController.php index a5adb17b7..923285cec 100644 --- a/src/applications/fund/controller/FundInitiativeBackController.php +++ b/src/applications/fund/controller/FundInitiativeBackController.php @@ -1,128 +1,129 @@ <?php final class FundInitiativeBackController 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(); } $merchant = id(new PhortuneMerchantQuery()) ->setViewer($viewer) ->withPHIDs(array($initiative->getMerchantPHID())) ->executeOne(); if (!$merchant) { return new Aphront404Response(); } $initiative_uri = '/'.$initiative->getMonogram(); if ($initiative->isClosed()) { return $this->newDialog() ->setTitle(pht('Initiative Closed')) ->appendParagraph( pht('You can not back a closed initiative.')) ->addCancelButton($initiative_uri); } $v_amount = null; $e_amount = true; $errors = array(); if ($request->isFormPost()) { $v_amount = $request->getStr('amount'); if (!strlen($v_amount)) { $errors[] = pht( 'You must specify how much money you want to contribute to the '. 'initiative.'); $e_amount = pht('Required'); } else { try { $currency = PhortuneCurrency::newFromUserInput( $viewer, $v_amount); $currency->assertInRange('1.00 USD', null); } catch (Exception $ex) { $errors[] = $ex->getMessage(); $e_amount = pht('Invalid'); } } if (!$errors) { $backer = FundBacker::initializeNewBacker($viewer) ->setInitiativePHID($initiative->getPHID()) ->attachInitiative($initiative) ->setAmountAsCurrency($currency) ->save(); $product = id(new PhortuneProductQuery()) ->setViewer($viewer) ->withClassAndRef('FundBackerProduct', $initiative->getPHID()) ->executeOne(); $account = PhortuneAccountQuery::loadActiveAccountForUser( $viewer, PhabricatorContentSource::newFromRequest($request)); $cart_implementation = id(new FundBackerCart()) ->setInitiative($initiative); $cart = $account->newCart($viewer, $cart_implementation, $merchant); $purchase = $cart->newPurchase($viewer, $product); $purchase ->setBasePriceAsCurrency($currency) ->setMetadataValue('backerPHID', $backer->getPHID()) ->save(); $xactions = array(); $xactions[] = id(new FundBackerTransaction()) ->setTransactionType(FundBackerTransaction::TYPE_STATUS) ->setNewValue(FundBacker::STATUS_IN_CART); $editor = id(new FundBackerEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request); $editor->applyTransactions($backer, $xactions); $cart->activateCart(); return id(new AphrontRedirectResponse()) ->setURI($cart->getCheckoutURI()); } } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setName('amount') ->setLabel(pht('Amount')) ->setValue($v_amount) ->setError($e_amount)); return $this->newDialog() - ->setTitle(pht('Back Initiative')) + ->setTitle( + pht('Back %s %s', $initiative->getMonogram(), $initiative->getName())) ->setErrors($errors) ->appendChild($form->buildLayoutView()) ->addCancelButton($initiative_uri) ->addSubmitButton(pht('Continue')); } } diff --git a/src/applications/fund/controller/FundInitiativeEditController.php b/src/applications/fund/controller/FundInitiativeEditController.php index 381bb88f9..b6b69945b 100644 --- a/src/applications/fund/controller/FundInitiativeEditController.php +++ b/src/applications/fund/controller/FundInitiativeEditController.php @@ -1,247 +1,259 @@ <?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(); $e_merchant = null; $v_merchant = $initiative->getMerchantPHID(); $v_desc = $initiative->getDescription(); + $v_risk = $initiative->getRisks(); 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_risk = $request->getStr('risks'); $v_view = $request->getStr('viewPolicy'); $v_edit = $request->getStr('editPolicy'); $v_merchant = $request->getStr('merchantPHID'); $v_projects = $request->getArr('projects'); $type_name = FundInitiativeTransaction::TYPE_NAME; $type_desc = FundInitiativeTransaction::TYPE_DESCRIPTION; + $type_risk = FundInitiativeTransaction::TYPE_RISKS; $type_merchant = FundInitiativeTransaction::TYPE_MERCHANT; $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_risk) + ->setNewValue($v_risk); + $xactions[] = id(new FundInitiativeTransaction()) ->setTransactionType($type_merchant) ->setNewValue($v_merchant); $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); $e_merchant = $ex->getShortMessage($type_merchant); $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(); } $merchants = id(new PhortuneMerchantQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); $merchant_options = array(); foreach ($merchants as $merchant) { $merchant_options[$merchant->getPHID()] = pht( 'Merchant %d %s', $merchant->getID(), $merchant->getName()); } if ($v_merchant && empty($merchant_options[$v_merchant])) { $merchant_options = array( $v_merchant => pht('(Restricted Merchant)'), ) + $merchant_options; } if (!$merchant_options) { return $this->newDialog() ->setTitle(pht('No Valid Phortune Merchant Accounts')) ->appendParagraph( pht( 'You do not control any merchant accounts which can receive '. 'payments from this initiative. When you create an initiative, '. 'you need to specify a merchant account where funds will be paid '. 'to.')) ->appendParagraph( pht( 'Create a merchant account in the Phortune application before '. 'creating an initiative in Fund.')) ->addCancelButton($this->getApplicationURI()); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setValue($v_name) ->setError($e_name)) ->appendChild( id(new AphrontFormSelectControl()) ->setName('merchantPHID') ->setLabel(pht('Pay To Merchant')) ->setValue($v_merchant) ->setError($e_merchant) ->setOptions($merchant_options)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setName('description') ->setLabel(pht('Description')) ->setValue($v_desc)) + ->appendChild( + id(new PhabricatorRemarkupControl()) + ->setName('risks') + ->setLabel(pht('Risks/Challenges')) + ->setValue($v_risk)) ->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/FundInitiativeViewController.php b/src/applications/fund/controller/FundInitiativeViewController.php index 2e5c5ca02..ee5231593 100644 --- a/src/applications/fund/controller/FundInitiativeViewController.php +++ b/src/applications/fund/controller/FundInitiativeViewController.php @@ -1,176 +1,187 @@ <?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(); $merchant_phid = $initiative->getMerchantPHID(); $this->loadHandles( array( $owner_phid, $merchant_phid, )); $view->addProperty( pht('Owner'), $this->getHandle($owner_phid)->renderLink()); $view->addProperty( - pht('Payable To Merchant'), + pht('Payable to Merchant'), $this->getHandle($merchant_phid)->renderLink()); $view->addProperty( pht('Total Funding'), $initiative->getTotalAsCurrency()->formatForDisplay()); $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); } + $risks = $initiative->getRisks(); + if (strlen($risks)) { + $risks = PhabricatorMarkupEngine::renderOneObject( + id(new PhabricatorMarkupOneOff())->setContent($risks), + 'default', + $viewer); + + $view->addSectionHeader(pht('Risks/Challenges')); + $view->addTextContent($risks); + } + 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}/"))); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Back Initiative')) ->setIcon('fa-money') ->setDisabled($initiative->isClosed()) ->setWorkflow(true) ->setHref($this->getApplicationURI("/back/{$id}/"))); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View Backers')) ->setIcon('fa-bank') ->setHref($this->getApplicationURI("/backers/{$id}/"))); return $view; } } diff --git a/src/applications/fund/editor/FundInitiativeEditor.php b/src/applications/fund/editor/FundInitiativeEditor.php index 2bbf311cc..cbb4074a7 100644 --- a/src/applications/fund/editor/FundInitiativeEditor.php +++ b/src/applications/fund/editor/FundInitiativeEditor.php @@ -1,192 +1,200 @@ <?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_RISKS; $types[] = FundInitiativeTransaction::TYPE_STATUS; $types[] = FundInitiativeTransaction::TYPE_BACKER; $types[] = FundInitiativeTransaction::TYPE_MERCHANT; $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_RISKS: + return $object->getRisks(); case FundInitiativeTransaction::TYPE_STATUS: return $object->getStatus(); case FundInitiativeTransaction::TYPE_BACKER: return null; case FundInitiativeTransaction::TYPE_MERCHANT: return $object->getMerchantPHID(); } 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_RISKS: case FundInitiativeTransaction::TYPE_STATUS: case FundInitiativeTransaction::TYPE_BACKER: case FundInitiativeTransaction::TYPE_MERCHANT: 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_RISKS: + $object->setRisks($xaction->getNewValue()); + return; case FundInitiativeTransaction::TYPE_MERCHANT: $object->setMerchantPHID($xaction->getNewValue()); return; case FundInitiativeTransaction::TYPE_STATUS: $object->setStatus($xaction->getNewValue()); return; case FundInitiativeTransaction::TYPE_BACKER: $backer = id(new FundBackerQuery()) ->setViewer($this->requireActor()) ->withPHIDs(array($xaction->getNewValue())) ->executeOne(); if (!$backer) { throw new Exception(pht('No such backer!')); } $backer_amount = $backer->getAmountAsCurrency(); $total = $object->getTotalAsCurrency()->add($backer_amount); $object->setTotalAsCurrency($total); 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_RISKS: case FundInitiativeTransaction::TYPE_STATUS: case FundInitiativeTransaction::TYPE_MERCHANT: case FundInitiativeTransaction::TYPE_BACKER: // TODO: Maybe we should apply the backer transaction from here? return; 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; case FundInitiativeTransaction::TYPE_MERCHANT: $missing = $this->validateIsEmptyTextField( $object->getName(), $xactions); if ($missing) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), pht('Payable merchant is required.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } else if ($xactions) { $merchant_phid = last($xactions)->getNewValue(); // Make sure the actor has permission to edit the merchant they're // selecting. You aren't allowed to send payments to an account you // do not control. $merchants = id(new PhortuneMerchantQuery()) ->setViewer($this->requireActor()) ->withPHIDs(array($merchant_phid)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); if (!$merchants) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht( 'You must specify a merchant account you control as the '. 'recipient of funds from this initiative.'), last($xactions)); $errors[] = $error; } } break; } return $errors; } } diff --git a/src/applications/fund/query/FundBackerSearchEngine.php b/src/applications/fund/query/FundBackerSearchEngine.php index 316f1944a..4ad4b7c53 100644 --- a/src/applications/fund/query/FundBackerSearchEngine.php +++ b/src/applications/fund/query/FundBackerSearchEngine.php @@ -1,154 +1,160 @@ <?php final class FundBackerSearchEngine extends PhabricatorApplicationSearchEngine { private $initiative; public function setInitiative(FundInitiative $initiative) { $this->initiative = $initiative; return $this; } public function getInitiative() { return $this->initiative; } public function getResultTypeDescription() { return pht('Fund Backers'); } public function getApplicationClassName() { return 'PhabricatorFundApplication'; } public function buildSavedQueryFromRequest(AphrontRequest $request) { $saved = new PhabricatorSavedQuery(); $saved->setParameter( 'backerPHIDs', $this->readUsersFromRequest($request, 'backers')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new FundBackerQuery()); $query->withStatuses(array(FundBacker::STATUS_PURCHASED)); if ($this->getInitiative()) { $query->withInitiativePHIDs( array( $this->getInitiative()->getPHID(), )); } $backer_phids = $saved->getParameter('backerPHIDs'); if ($backer_phids) { $query->withBackerPHIDs($backer_phids); } return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $backer_phids = $saved->getParameter('backerPHIDs', array()); $all_phids = array_mergev( array( $backer_phids, )); $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireViewer()) ->withPHIDs($all_phids) ->execute(); $form ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Backers')) ->setName('backers') ->setDatasource(new PhabricatorPeopleDatasource()) ->setValue(array_select_keys($handles, $backer_phids))); } protected function getURI($path) { if ($this->getInitiative()) { return '/fund/backers/'.$this->getInitiative()->getID().'/'.$path; } else { return '/fund/backers/'.$path; } } public function getBuiltinQueryNames() { $names = array(); $names['all'] = pht('All Backers'); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } protected function getRequiredHandlePHIDsForResultList( array $backers, PhabricatorSavedQuery $query) { $phids = array(); foreach ($backers as $backer) { $phids[] = $backer->getBackerPHID(); $phids[] = $backer->getInitiativePHID(); } return $phids; } protected function renderResultList( array $backers, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($backers, 'FundBacker'); $viewer = $this->requireViewer(); - $list = id(new PHUIObjectItemListView()); + $rows = array(); foreach ($backers as $backer) { - $backer_handle = $handles[$backer->getBackerPHID()]; - - $currency = $backer->getAmountAsCurrency(); - - $header = pht( - '%s for %s', - $currency->formatForDisplay(), - $handles[$backer->getInitiativePHID()]->renderLink()); - - $item = id(new PHUIObjectItemView()) - ->setHeader($header) - ->addIcon( - 'none', - phabricator_datetime($backer->getDateCreated(), $viewer)) - ->addByline(pht('Backer: %s', $backer_handle->renderLink())); - - $list->addItem($item); + $rows[] = array( + $handles[$backer->getInitiativePHID()]->renderLink(), + $handles[$backer->getBackerPHID()]->renderLink(), + $backer->getAmountAsCurrency()->formatForDisplay(), + phabricator_datetime($backer->getDateCreated(), $viewer), + ); } + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('Initiative'), + pht('Backer'), + pht('Amount'), + pht('Date'), + )) + ->setColumnClasses( + array( + null, + null, + 'wide right', + 'right', + )); - return $list; + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Backers')) + ->appendChild($table); } } diff --git a/src/applications/fund/storage/FundInitiative.php b/src/applications/fund/storage/FundInitiative.php index f9ed49eab..7d9ce352b 100644 --- a/src/applications/fund/storage/FundInitiative.php +++ b/src/applications/fund/storage/FundInitiative.php @@ -1,179 +1,180 @@ <?php final class FundInitiative extends FundDAO implements PhabricatorPolicyInterface, PhabricatorProjectInterface, PhabricatorApplicationTransactionInterface, PhabricatorSubscribableInterface, PhabricatorMentionableInterface, PhabricatorFlaggableInterface, PhabricatorTokenReceiverInterface, PhabricatorDestructibleInterface { protected $name; protected $ownerPHID; protected $merchantPHID; protected $description; + protected $risks; protected $viewPolicy; protected $editPolicy; protected $status; protected $totalAsCurrency; 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) ->setTotalAsCurrency(PhortuneCurrency::newEmptyCurrency()); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'description' => 'text', 'status' => 'text32', 'merchantPHID' => 'phid?', 'totalAsCurrency' => 'text64', ), self::CONFIG_APPLICATION_SERIALIZERS => array( 'totalAsCurrency' => new PhortuneCurrencySerializer(), ), self::CONFIG_KEY_SCHEMA => array( 'key_status' => array( 'columns' => array('status'), ), 'key_owner' => array( 'columns' => array('ownerPHID'), ), ), ) + 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 ($viewer->getPHID() == $this->getOwnerPHID()); } public function describeAutomaticCapability($capability) { return pht( 'The owner of an initiative can always view and edit it.'); } /* -( 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 index 30eb45806..c128d01a2 100644 --- a/src/applications/fund/storage/FundInitiativeTransaction.php +++ b/src/applications/fund/storage/FundInitiativeTransaction.php @@ -1,182 +1,189 @@ <?php final class FundInitiativeTransaction extends PhabricatorApplicationTransaction { const TYPE_NAME = 'fund:name'; const TYPE_DESCRIPTION = 'fund:description'; + const TYPE_RISKS = 'fund:risks'; const TYPE_STATUS = 'fund:status'; const TYPE_BACKER = 'fund:backer'; const TYPE_MERCHANT = 'fund:merchant'; public function getApplicationName() { return 'fund'; } public function getApplicationTransactionType() { return FundInitiativePHIDType::TYPECONST; } public function getApplicationTransactionCommentObject() { return null; } public function getRequiredHandlePHIDs() { $phids = parent::getRequiredHandlePHIDs(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { case FundInitiativeTransaction::TYPE_MERCHANT: if ($old) { $phids[] = $old; } if ($new) { $phids[] = $new; } break; } return $phids; } 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_RISKS: + return pht( + '%s edited the risks for this initiative.', + $this->renderHandleLink($author_phid)); 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; case FundInitiativeTransaction::TYPE_BACKER: return pht( '%s backed this initiative.', $this->renderHandleLink($author_phid)); case FundInitiativeTransaction::TYPE_MERCHANT: if ($old === null) { return pht( '%s set this initiative to pay to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($new)); } else { return pht( '%s changed the merchant receiving funds from this '. 'initiative from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($old), $this->renderHandleLink($new)); } } 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; case FundInitiativeTransaction::TYPE_BACKER: return pht( '%s backed %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } return parent::getTitleForFeed($story); } public function shouldHide() { $old = $this->getOldValue(); switch ($this->getTransactionType()) { case FundInitiativeTransaction::TYPE_DESCRIPTION: + case FundInitiativeTransaction::TYPE_RISKS: return ($old === null); } return parent::shouldHide(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case FundInitiativeTransaction::TYPE_DESCRIPTION: + case FundInitiativeTransaction::TYPE_RISKS: return ($this->getOldValue() !== null); } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } }