diff --git a/resources/sql/autopatches/20150806.ponder.status.1.sql b/resources/sql/autopatches/20150806.ponder.status.1.sql new file mode 100644 index 0000000000..b0307ca2dc --- /dev/null +++ b/resources/sql/autopatches/20150806.ponder.status.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_ponder.ponder_question + MODIFY status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20150806.ponder.status.2.sql b/resources/sql/autopatches/20150806.ponder.status.2.sql new file mode 100644 index 0000000000..89371cc505 --- /dev/null +++ b/resources/sql/autopatches/20150806.ponder.status.2.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_ponder.ponder_question + SET status = 'open' WHERE status = 0; diff --git a/resources/sql/autopatches/20150806.ponder.status.3.sql b/resources/sql/autopatches/20150806.ponder.status.3.sql new file mode 100644 index 0000000000..3a8f5c719d --- /dev/null +++ b/resources/sql/autopatches/20150806.ponder.status.3.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_ponder.ponder_question + SET status = 'resolved' WHERE status = 1; diff --git a/src/applications/ponder/application/PhabricatorPonderApplication.php b/src/applications/ponder/application/PhabricatorPonderApplication.php index 11e90dc274..6ba5e3ca4f 100644 --- a/src/applications/ponder/application/PhabricatorPonderApplication.php +++ b/src/applications/ponder/application/PhabricatorPonderApplication.php @@ -1,113 +1,113 @@ [1-9]\d*)' => 'PonderQuestionViewController', '/ponder/' => array( '(?:query/(?P[^/]+)/)?' => 'PonderQuestionListController', 'answer/add/' => 'PonderAnswerSaveController', 'answer/edit/(?P\d+)/' => 'PonderAnswerEditController', 'answer/comment/(?P\d+)/' => 'PonderAnswerCommentController', 'answer/history/(?P\d+)/' => 'PonderAnswerHistoryController', 'question/edit/(?:(?P\d+)/)?' => 'PonderQuestionEditController', 'question/create/' => 'PonderQuestionEditController', 'question/comment/(?P\d+)/' => 'PonderQuestionCommentController', 'question/history/(?P\d+)/' => 'PonderQuestionHistoryController', 'preview/' => 'PhabricatorMarkupPreviewController', - 'question/(?Popen|close)/(?P[1-9]\d*)/' + 'question/status/(?P[1-9]\d*)/' => 'PonderQuestionStatusController', 'vote/' => 'PonderVoteSaveController', ), ); } public function getMailCommandObjects() { return array( 'question' => array( 'name' => pht('Email Commands: Questions'), 'header' => pht('Interacting with Ponder Questions'), 'object' => new PonderQuestion(), 'summary' => pht( 'This page documents the commands you can use to interact with '. 'questions in Ponder.'), ), ); } protected function getCustomCapabilities() { return array( PonderQuestionDefaultViewCapability::CAPABILITY => array( 'template' => PonderQuestionPHIDType::TYPECONST, 'capability' => PhabricatorPolicyCapability::CAN_VIEW, ), PonderQuestionDefaultEditCapability::CAPABILITY => array( 'template' => PonderQuestionPHIDType::TYPECONST, 'capability' => PhabricatorPolicyCapability::CAN_EDIT, ), ); } public function getApplicationSearchDocumentTypes() { return array( PonderQuestionPHIDType::TYPECONST, ); } } diff --git a/src/applications/ponder/constants/PonderQuestionStatus.php b/src/applications/ponder/constants/PonderQuestionStatus.php index 2d26b9cb66..6c612ec4ae 100644 --- a/src/applications/ponder/constants/PonderQuestionStatus.php +++ b/src/applications/ponder/constants/PonderQuestionStatus.php @@ -1,41 +1,80 @@ pht('Open'), - self::STATUS_CLOSED => pht('Closed'), + self::STATUS_OPEN => pht('Open'), + self::STATUS_CLOSED_RESOLVED => pht('Closed, Resolved'), + self::STATUS_CLOSED_OBSOLETE => pht('Closed, Obsolete'), + self::STATUS_CLOSED_DUPLICATE => pht('Closed, Duplicate'), ); } public static function getQuestionStatusFullName($status) { $map = array( - self::STATUS_OPEN => pht('Open'), - self::STATUS_CLOSED => pht('Closed by author'), + self::STATUS_OPEN => pht('Open'), + self::STATUS_CLOSED_RESOLVED => pht('Closed, Resolved'), + self::STATUS_CLOSED_OBSOLETE => pht('Closed, Obsolete'), + self::STATUS_CLOSED_DUPLICATE => pht('Closed, Duplicate'), + ); + return idx($map, $status, pht('Unknown')); + } + + public static function getQuestionStatusDescription($status) { + $map = array( + self::STATUS_OPEN => + pht('This question is open for answers.'), + self::STATUS_CLOSED_RESOLVED => + pht('This question has been resolved.'), + self::STATUS_CLOSED_OBSOLETE => + pht('This question is no longer valid or out of date.'), + self::STATUS_CLOSED_DUPLICATE => + pht('This question is a duplicate of another question.'), ); return idx($map, $status, pht('Unknown')); } public static function getQuestionStatusTagColor($status) { $map = array( self::STATUS_OPEN => PHUITagView::COLOR_BLUE, - self::STATUS_CLOSED => PHUITagView::COLOR_BLACK, + self::STATUS_CLOSED_RESOLVED => PHUITagView::COLOR_BLACK, + self::STATUS_CLOSED_OBSOLETE => PHUITagView::COLOR_BLACK, + self::STATUS_CLOSED_DUPLICATE => PHUITagView::COLOR_BLACK, ); return idx($map, $status); } public static function getQuestionStatusIcon($status) { $map = array( self::STATUS_OPEN => 'fa-question-circle', - self::STATUS_CLOSED => 'fa-check-square-o', + self::STATUS_CLOSED_RESOLVED => 'fa-check', + self::STATUS_CLOSED_OBSOLETE => 'fa-ban', + self::STATUS_CLOSED_DUPLICATE => 'fa-clone', ); return idx($map, $status); } + public static function getQuestionStatusOpenMap() { + return array( + self::STATUS_OPEN, + ); + } + + public static function getQuestionStatusClosedMap() { + return array( + self::STATUS_CLOSED_RESOLVED, + self::STATUS_CLOSED_OBSOLETE, + self::STATUS_CLOSED_DUPLICATE, + ); + } + + } diff --git a/src/applications/ponder/controller/PonderQuestionEditController.php b/src/applications/ponder/controller/PonderQuestionEditController.php index cf8b5f9de1..d1a8d48f59 100644 --- a/src/applications/ponder/controller/PonderQuestionEditController.php +++ b/src/applications/ponder/controller/PonderQuestionEditController.php @@ -1,178 +1,193 @@ getViewer(); $id = $request->getURIData('id'); if ($id) { $question = id(new PonderQuestionQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$question) { return new Aphront404Response(); } $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( $question->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $v_projects = array_reverse($v_projects); } else { $question = PonderQuestion::initializeNewQuestion($viewer); $v_projects = array(); } $v_title = $question->getTitle(); $v_content = $question->getContent(); $v_view = $question->getViewPolicy(); $v_edit = $question->getEditPolicy(); $v_space = $question->getSpacePHID(); + $v_status = $question->getStatus(); + $errors = array(); $e_title = true; if ($request->isFormPost()) { $v_title = $request->getStr('title'); $v_content = $request->getStr('content'); $v_projects = $request->getArr('projects'); $v_view = $request->getStr('viewPolicy'); $v_edit = $request->getStr('editPolicy'); $v_space = $request->getStr('spacePHID'); + $v_status = $request->getStr('status'); $len = phutil_utf8_strlen($v_title); if ($len < 1) { $errors[] = pht('Title must not be empty.'); $e_title = pht('Required'); } else if ($len > 255) { $errors[] = pht('Title is too long.'); $e_title = pht('Too Long'); } if (!$errors) { $template = id(new PonderQuestionTransaction()); $xactions = array(); $xactions[] = id(clone $template) ->setTransactionType(PonderQuestionTransaction::TYPE_TITLE) ->setNewValue($v_title); $xactions[] = id(clone $template) ->setTransactionType(PonderQuestionTransaction::TYPE_CONTENT) ->setNewValue($v_content); + $xactions[] = id(clone $template) + ->setTransactionType(PonderQuestionTransaction::TYPE_STATUS) + ->setNewValue($v_status); + $xactions[] = id(clone $template) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($v_view); $xactions[] = id(clone $template) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($v_edit); $xactions[] = id(clone $template) ->setTransactionType(PhabricatorTransactions::TYPE_SPACE) ->setNewValue($v_space); $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new PonderQuestionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $proj_edge_type) ->setNewValue(array('=' => array_fuse($v_projects))); $editor = id(new PonderQuestionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); $editor->applyTransactions($question, $xactions); return id(new AphrontRedirectResponse()) ->setURI('/Q'.$question->getID()); } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($question) ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Question')) ->setName('title') ->setValue($v_title) ->setError($e_title)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($viewer) ->setName('content') ->setID('content') ->setValue($v_content) ->setLabel(pht('Description')) ->setUser($viewer)) ->appendControl( id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($question) ->setSpacePHID($v_space) ->setPolicies($policies) ->setValue($v_view) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) ->appendControl( id(new AphrontFormPolicyControl()) ->setName('editPolicy') ->setPolicyObject($question) ->setPolicies($policies) ->setValue($v_edit) - ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)); + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel(pht('Status')) + ->setName('status') + ->setValue($v_status) + ->setOptions(PonderQuestionStatus::getQuestionStatusMap())); $form->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setValue($v_projects) ->setDatasource(new PhabricatorProjectDatasource())); $form->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($this->getApplicationURI()) ->setValue(pht('Ask Away!'))); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Question Preview')) ->setControlID('content') ->setPreviewURI($this->getApplicationURI('preview/')); - $form_box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Ask New Question')) - ->setFormErrors($errors) - ->setForm($form); - $crumbs = $this->buildApplicationCrumbs(); $id = $question->getID(); if ($id) { $crumbs->addTextCrumb("Q{$id}", "/Q{$id}"); $crumbs->addTextCrumb(pht('Edit')); + $title = pht('Edit Question'); } else { $crumbs->addTextCrumb(pht('Ask Question')); + $title = pht('Ask New Question'); } + $form_box = id(new PHUIObjectBoxView()) + ->setHeaderText($title) + ->setFormErrors($errors) + ->setForm($form); + return $this->buildApplicationPage( array( $crumbs, $form_box, $preview, ), array( - 'title' => pht('Ask New Question'), + 'title' => $title, )); } } diff --git a/src/applications/ponder/controller/PonderQuestionStatusController.php b/src/applications/ponder/controller/PonderQuestionStatusController.php index db2d971c92..a4671d5915 100644 --- a/src/applications/ponder/controller/PonderQuestionStatusController.php +++ b/src/applications/ponder/controller/PonderQuestionStatusController.php @@ -1,49 +1,65 @@ getViewer(); $id = $request->getURIData('id'); - $status = $request->getURIData('status'); $question = id(new PonderQuestionQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$question) { return new Aphront404Response(); } - switch ($status) { - case 'open': - $status = PonderQuestionStatus::STATUS_OPEN; - break; - case 'close': - $status = PonderQuestionStatus::STATUS_CLOSED; - break; - default: - return new Aphront400Response(); + $view_uri = '/Q'.$question->getID(); + $v_status = $question->getStatus(); + + if ($request->isFormPost()) { + $v_status = $request->getStr('status'); + + $xactions = array(); + $xactions[] = id(new PonderQuestionTransaction()) + ->setTransactionType(PonderQuestionTransaction::TYPE_STATUS) + ->setNewValue($v_status); + + $editor = id(new PonderQuestionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($question, $xactions); + + return id(new AphrontRedirectResponse())->setURI($view_uri); } - $xactions = array(); - $xactions[] = id(new PonderQuestionTransaction()) - ->setTransactionType(PonderQuestionTransaction::TYPE_STATUS) - ->setNewValue($status); + $radio = id(new AphrontFormRadioButtonControl()) + ->setLabel(pht('Status')) + ->setName('status') + ->setValue($v_status); + + foreach (PonderQuestionStatus::getQuestionStatusMap() as $value => $name) { + $description = PonderQuestionStatus::getQuestionStatusDescription($value); + $radio->addButton($value, $name, $description); + } - $editor = id(new PonderQuestionEditor()) - ->setActor($viewer) - ->setContentSourceFromRequest($request); + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild($radio); - $editor->applyTransactions($question, $xactions); + return $this->newDialog() + ->setTitle(pht('Change Question Status')) + ->appendChild($form->buildLayoutView()) + ->addSubmitButton(pht('Submit')) + ->addCancelButton($view_uri); - return id(new AphrontRedirectResponse())->setURI('/Q'.$question->getID()); } } diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php index 51f165b408..c5349c5eab 100644 --- a/src/applications/ponder/controller/PonderQuestionViewController.php +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -1,403 +1,404 @@ getViewer(); $id = $request->getURIData('id'); $question = id(new PonderQuestionQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needAnswers(true) ->needViewerVotes(true) ->executeOne(); if (!$question) { return new Aphront404Response(); } $question->attachVotes($viewer->getPHID()); $question_xactions = $this->buildQuestionTransactions($question); $answers = $this->buildAnswers($question->getAnswers()); $authors = mpull($question->getAnswers(), null, 'getAuthorPHID'); if (isset($authors[$viewer->getPHID()])) { $answer_add_panel = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->appendChild( pht( 'You have already answered this question. You can not answer '. 'twice, but you can edit your existing answer.')); } else { $answer_add_panel = new PonderAddAnswerView(); $answer_add_panel ->setQuestion($question) ->setUser($viewer) ->setActionURI('/ponder/answer/add/'); } $header = new PHUIHeaderView(); $header->setHeader($question->getTitle()); $header->setUser($viewer); $header->setPolicyObject($question); if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) { $header->setStatus('fa-square-o', 'bluegrey', pht('Open')); } else { - $header->setStatus('fa-check-square-o', 'dark', pht('Closed')); + $text = PonderQuestionStatus::getQuestionStatusFullName( + $question->getStatus()); + $icon = PonderQuestionStatus::getQuestionStatusIcon( + $question->getStatus()); + $header->setStatus($icon, 'dark', $text); } $actions = $this->buildActionListView($question); $properties = $this->buildPropertyListView($question, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); $crumbs->addTextCrumb('Q'.$id, '/Q'.$id); $ponder_view = phutil_tag( 'div', array( 'class' => 'ponder-question-view', ), array( $crumbs, $object_box, $question_xactions, $answers, $answer_add_panel, )); return $this->buildApplicationPage( array( $ponder_view, ), array( 'title' => 'Q'.$question->getID().' '.$question->getTitle(), 'pageObjects' => array_merge( array($question->getPHID()), mpull($question->getAnswers(), 'getPHID')), )); } private function buildActionListView(PonderQuestion $question) { $viewer = $this->getViewer(); $request = $this->getRequest(); $id = $question->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $question, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($question) ->setObjectURI($request->getRequestURI()); $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Question')) ->setHref($this->getApplicationURI("/question/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) { $name = pht('Close Question'); $icon = 'fa-check-square-o'; - $href = 'close'; } else { $name = pht('Reopen Question'); $icon = 'fa-square-o'; - $href = 'open'; } $view->addAction( id(new PhabricatorActionView()) ->setName($name) ->setIcon($icon) - ->setRenderAsForm($can_edit) - ->setWorkflow(!$can_edit) + ->setWorkflow(true) ->setDisabled(!$can_edit) - ->setHref($this->getApplicationURI("/question/{$href}/{$id}/"))); + ->setHref($this->getApplicationURI("/question/status/{$id}/"))); $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-list') ->setName(pht('View History')) ->setHref($this->getApplicationURI("/question/history/{$id}/"))); return $view; } private function buildPropertyListView( PonderQuestion $question, PhabricatorActionListView $actions) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($question) ->setActionList($actions); $view->addProperty( pht('Author'), $viewer->renderHandle($question->getAuthorPHID())); $view->addProperty( pht('Created'), phabricator_datetime($question->getDateCreated(), $viewer)); $view->invokeWillRenderEvent(); $votable = id(new PonderVotableView()) ->setPHID($question->getPHID()) ->setURI($this->getApplicationURI('vote/')) ->setCount($question->getVoteCount()) ->setVote($question->getUserVote()); $view->addSectionHeader(pht('Question')); $view->addTextContent( array( $votable, phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), PhabricatorMarkupEngine::renderOneObject( $question, $question->getMarkupField(), $viewer)), )); return $view; } private function buildQuestionTransactions(PonderQuestion $question) { $viewer = $this->getViewer(); $id = $question->getID(); $timeline = $this->buildTransactionTimeline( $question, id(new PonderQuestionTransactionQuery()) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))); $xactions = $timeline->getTransactions(); $add_comment = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($question->getPHID()) ->setShowPreview(false) ->setHeaderText(pht('Question Comment')) ->setAction($this->getApplicationURI("/question/comment/{$id}/")) ->setSubmitButtonName(pht('Comment')); return $this->wrapComments( count($xactions), array( $timeline, $add_comment, )); } /** * This is fairly non-standard; building N timelines at once (N = number of * answers) is tricky business. * * TODO - re-factor this to ajax in one answer panel at a time in a more * standard fashion. This is necessary to scale this application. */ private function buildAnswers(array $answers) { $viewer = $this->getViewer(); $out = array(); $xactions = id(new PonderAnswerTransactionQuery()) ->setViewer($viewer) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT)) ->withObjectPHIDs(mpull($answers, 'getPHID')) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $xaction_groups = mgroup($xactions, 'getObjectPHID'); foreach ($answers as $answer) { $author_phid = $answer->getAuthorPHID(); $xactions = idx($xaction_groups, $answer->getPHID(), array()); $id = $answer->getID(); $out[] = phutil_tag('br'); $out[] = phutil_tag('br'); $out[] = id(new PhabricatorAnchorView()) ->setAnchorName("A$id"); $header = id(new PHUIHeaderView()) ->setHeader($viewer->renderHandle($author_phid)); $actions = $this->buildAnswerActions($answer); $properties = $this->buildAnswerProperties($answer, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $out[] = $object_box; $details = array(); $details[] = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($answer->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($answer->getPHID()) ->setShowPreview(false) ->setHeaderText(pht('Answer Comment')) ->setAction($this->getApplicationURI("/answer/comment/{$id}/")) ->setSubmitButtonName(pht('Comment')); $details[] = $form; $out[] = $this->wrapComments( count($xactions), $details); } $out[] = phutil_tag('br'); $out[] = phutil_tag('br'); return $out; } private function buildAnswerActions(PonderAnswer $answer) { $viewer = $this->getViewer(); $request = $this->getRequest(); $id = $answer->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $answer, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($answer) ->setObjectURI($request->getRequestURI()); $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Answer')) ->setHref($this->getApplicationURI("/answer/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-list') ->setName(pht('View History')) ->setHref($this->getApplicationURI("/answer/history/{$id}/"))); return $view; } private function buildAnswerProperties( PonderAnswer $answer, PhabricatorActionListView $actions) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($answer) ->setActionList($actions); $view->addProperty( pht('Created'), phabricator_datetime($answer->getDateCreated(), $viewer)); $view->invokeWillRenderEvent(); $votable = id(new PonderVotableView()) ->setPHID($answer->getPHID()) ->setURI($this->getApplicationURI('vote/')) ->setCount($answer->getVoteCount()) ->setVote($answer->getUserVote()); $view->addSectionHeader(pht('Answer')); $view->addTextContent( array( $votable, phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), PhabricatorMarkupEngine::renderOneObject( $answer, $answer->getMarkupField(), $viewer)), )); return $view; } private function wrapComments($n, $stuff) { if ($n == 0) { $text = pht('Add a Comment'); } else { $text = pht('Show %s Comments', new PhutilNumber($n)); } $show_id = celerity_generate_unique_node_id(); $hide_id = celerity_generate_unique_node_id(); Javelin::initBehavior('phabricator-reveal-content'); require_celerity_resource('ponder-view-css'); $show = phutil_tag( 'div', array( 'id' => $show_id, 'class' => 'ponder-show-comments', ), javelin_tag( 'a', array( 'href' => '#', 'sigil' => 'reveal-content', 'meta' => array( 'showIDs' => array($hide_id), 'hideIDs' => array($show_id), ), ), $text)); $hide = phutil_tag( 'div', array( 'class' => 'ponder-comments-view', 'id' => $hide_id, 'style' => 'display: none', ), $stuff); return array($show, $hide); } } diff --git a/src/applications/ponder/query/PonderQuestionQuery.php b/src/applications/ponder/query/PonderQuestionQuery.php index 74d8230327..1ed3344382 100644 --- a/src/applications/ponder/query/PonderQuestionQuery.php +++ b/src/applications/ponder/query/PonderQuestionQuery.php @@ -1,203 +1,184 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAuthorPHIDs(array $phids) { $this->authorPHIDs = $phids; return $this; } - public function withStatus($status) { + public function withStatuses($status) { $this->status = $status; return $this; } public function withAnswererPHIDs(array $phids) { $this->answererPHIDs = $phids; return $this; } public function needAnswers($need_answers) { $this->needAnswers = $need_answers; return $this; } public function needViewerVotes($need_viewer_votes) { $this->needViewerVotes = $need_viewer_votes; return $this; } public function needProjectPHIDs($need_projects) { $this->needProjectPHIDs = $need_projects; return $this; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'q.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'q.phid IN (%Ls)', $this->phids); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( $conn, 'q.authorPHID IN (%Ls)', $this->authorPHIDs); } if ($this->status !== null) { - switch ($this->status) { - case self::STATUS_ANY: - break; - case self::STATUS_OPEN: - $where[] = qsprintf( - $conn, - 'q.status = %d', - PonderQuestionStatus::STATUS_OPEN); - break; - case self::STATUS_CLOSED: - $where[] = qsprintf( - $conn, - 'q.status = %d', - PonderQuestionStatus::STATUS_CLOSED); - break; - default: - throw new Exception(pht("Unknown status query '%s'!", $this->status)); - } + $where[] = qsprintf( + $conn, + 'q.status IN (%Ls)', + $this->status); } return $where; } public function newResultObject() { return new PonderQuestion(); } protected function loadPage() { return $this->loadStandardPage(new PonderQuestion()); } protected function willFilterPage(array $questions) { $phids = mpull($questions, 'getPHID'); if ($this->needAnswers) { $aquery = id(new PonderAnswerQuery()) ->setViewer($this->getViewer()) ->setOrderVector(array('-id')) ->withQuestionIDs(mpull($questions, 'getID')); if ($this->needViewerVotes) { $aquery->needViewerVotes($this->needViewerVotes); } $answers = $aquery->execute(); $answers = mgroup($answers, 'getQuestionID'); foreach ($questions as $question) { $question_answers = idx($answers, $question->getID(), array()); $question->attachAnswers(mpull($question_answers, null, 'getPHID')); } } if ($this->needViewerVotes) { $viewer_phid = $this->getViewer()->getPHID(); $etype = PonderQuestionHasVotingUserEdgeType::EDGECONST; $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($phids) ->withDestinationPHIDs(array($viewer_phid)) ->withEdgeTypes(array($etype)) ->needEdgeData(true) ->execute(); foreach ($questions as $question) { $user_edge = idx( $edges[$question->getPHID()][$etype], $viewer_phid, array()); $question->attachUserVote($viewer_phid, idx($user_edge, 'data', 0)); } } if ($this->needProjectPHIDs) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($phids) ->withEdgeTypes( array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, )); $edge_query->execute(); foreach ($questions as $question) { $project_phids = $edge_query->getDestinationPHIDs( array($question->getPHID())); $question->attachProjectPHIDs($project_phids); } } return $questions; } private function buildJoinsClause(AphrontDatabaseConnection $conn_r) { $joins = array(); if ($this->answererPHIDs) { $answer_table = new PonderAnswer(); $joins[] = qsprintf( $conn_r, 'JOIN %T a ON a.questionID = q.id AND a.authorPHID IN (%Ls)', $answer_table->getTableName(), $this->answererPHIDs); } return implode(' ', $joins); } protected function getPrimaryTableAlias() { return 'q'; } public function getQueryApplicationClass() { return 'PhabricatorPonderApplication'; } } diff --git a/src/applications/ponder/query/PonderQuestionSearchEngine.php b/src/applications/ponder/query/PonderQuestionSearchEngine.php index 1b475c7b58..b444ff8be9 100644 --- a/src/applications/ponder/query/PonderQuestionSearchEngine.php +++ b/src/applications/ponder/query/PonderQuestionSearchEngine.php @@ -1,177 +1,174 @@ needProjectPHIDs(true); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); if ($map['authorPHIDs']) { $query->withAuthorPHIDs($map['authorPHIDs']); } if ($map['answerers']) { $query->withAnswererPHIDs($map['answerers']); } - $status = $map['status']; - if ($status != null) { - switch ($status) { - case 0: - $query->withStatus(PonderQuestionQuery::STATUS_OPEN); - break; - case 1: - $query->withStatus(PonderQuestionQuery::STATUS_CLOSED); - break; - } + if ($map['statuses']) { + $query->withStatuses($map['statuses']); } return $query; } protected function buildCustomSearchFields() { return array( id(new PhabricatorUsersSearchField()) ->setKey('authorPHIDs') ->setAliases(array('authors')) ->setLabel(pht('Authors')), id(new PhabricatorUsersSearchField()) ->setKey('answerers') ->setAliases(array('answerers')) ->setLabel(pht('Answered By')), - id(new PhabricatorSearchSelectField()) + id(new PhabricatorSearchCheckboxesField()) ->setLabel(pht('Status')) - ->setKey('status') + ->setKey('statuses') ->setOptions(PonderQuestionStatus::getQuestionStatusMap()), ); } protected function getURI($path) { return '/ponder/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'open' => pht('Open Questions'), + 'resolved' => pht('Resolved Questions'), 'all' => pht('All Questions'), ); if ($this->requireViewer()->isLoggedIn()) { $names['authored'] = pht('Authored'); $names['answered'] = pht('Answered'); } return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; case 'open': - return $query->setParameter('status', PonderQuestionQuery::STATUS_OPEN); + return $query->setParameter( + 'statuses', array(PonderQuestionStatus::STATUS_OPEN)); + case 'resolved': + return $query->setParameter( + 'statuses', array(PonderQuestionStatus::STATUS_CLOSED_RESOLVED)); case 'authored': return $query->setParameter( 'authorPHIDs', array($this->requireViewer()->getPHID())); case 'answered': return $query->setParameter( 'answererPHIDs', array($this->requireViewer()->getPHID())); } return parent::buildSavedQueryFromBuiltin($query_key); } protected function getRequiredHandlePHIDsForResultList( array $questions, PhabricatorSavedQuery $query) { return mpull($questions, 'getAuthorPHID'); } protected function renderResultList( array $questions, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($questions, 'PonderQuestion'); $viewer = $this->requireViewer(); $proj_phids = array(); foreach ($questions as $question) { foreach ($question->getProjectPHIDs() as $project_phid) { $proj_phids[] = $project_phid; } } $proj_handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs($proj_phids) ->execute(); $view = id(new PHUIObjectItemListView()) ->setUser($viewer); foreach ($questions as $question) { $color = PonderQuestionStatus::getQuestionStatusTagColor( $question->getStatus()); $icon = PonderQuestionStatus::getQuestionStatusIcon( $question->getStatus()); $full_status = PonderQuestionStatus::getQuestionStatusFullName( $question->getStatus()); $item = new PHUIObjectItemView(); $item->setObjectName('Q'.$question->getID()); $item->setHeader($question->getTitle()); $item->setHref('/Q'.$question->getID()); $item->setObject($question); $item->setStatusIcon($icon.' '.$color, $full_status); $project_handles = array_select_keys( $proj_handles, $question->getProjectPHIDs()); $created_date = phabricator_date($question->getDateCreated(), $viewer); $item->addIcon('none', $created_date); $item->addByline( pht( 'Asked by %s', $handles[$question->getAuthorPHID()]->renderLink())); $item->addAttribute( pht('%d Answer(s)', $question->getAnswerCount())); if ($project_handles) { $item->addAttribute( id(new PHUIHandleTagListView()) ->setLimit(4) ->setSlim(true) ->setHandles($project_handles)); } $view->addItem($item); } $result = new PhabricatorApplicationSearchResultView(); $result->setObjectList($view); $result->setNoDataString(pht('No questions found.')); return $result; } } diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php index f2d81c4838..0e1ea0226b 100644 --- a/src/applications/ponder/storage/PonderQuestion.php +++ b/src/applications/ponder/storage/PonderQuestion.php @@ -1,343 +1,343 @@ setViewer($actor) ->withClasses(array('PhabricatorPonderApplication')) ->executeOne(); $view_policy = $app->getPolicy( PonderQuestionDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy( PonderQuestionDefaultEditCapability::CAPABILITY); return id(new PonderQuestion()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setStatus(PonderQuestionStatus::STATUS_OPEN) ->setVoteCount(0) ->setAnswerCount(0) ->setHeat(0.0) ->setSpacePHID($actor->getDefaultSpacePHID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'voteCount' => 'sint32', - 'status' => 'uint32', + 'status' => 'text32', 'content' => 'text', 'heat' => 'double', 'answerCount' => 'uint32', 'mailKey' => 'bytes20', // T6203/NULLABILITY // This should always exist. 'contentSource' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), 'heat' => array( 'columns' => array('heat'), ), 'status' => array( 'columns' => array('status'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(PonderQuestionPHIDType::TYPECONST); } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function attachVotes($user_phid) { $qa_phids = mpull($this->answers, 'getPHID') + array($this->getPHID()); $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($user_phid)) ->withDestinationPHIDs($qa_phids) ->withEdgeTypes( array( PonderVotingUserHasQuestionEdgeType::EDGECONST, PonderVotingUserHasAnswerEdgeType::EDGECONST, )) ->needEdgeData(true) ->execute(); $question_edge = $edges[$user_phid][PonderVotingUserHasQuestionEdgeType::EDGECONST]; $answer_edges = $edges[$user_phid][PonderVotingUserHasAnswerEdgeType::EDGECONST]; $edges = null; $this->setUserVote(idx($question_edge, $this->getPHID())); foreach ($this->answers as $answer) { $answer->setUserVote(idx($answer_edges, $answer->getPHID())); } } public function setUserVote($vote) { $this->vote = $vote['data']; if (!$this->vote) { $this->vote = PonderVote::VOTE_NONE; } return $this; } public function attachUserVote($user_phid, $vote) { $this->vote = $vote; return $this; } public function getUserVote() { return $this->vote; } public function setComments($comments) { $this->comments = $comments; return $this; } public function getComments() { return $this->comments; } public function attachAnswers(array $answers) { assert_instances_of($answers, 'PonderAnswer'); $this->answers = $answers; return $this; } public function getAnswers() { return $this->answers; } public function getProjectPHIDs() { return $this->assertAttached($this->projectPHIDs); } public function attachProjectPHIDs(array $phids) { $this->projectPHIDs = $phids; return $this; } public function getMarkupField() { return self::MARKUP_FIELD_CONTENT; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PonderQuestionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PonderQuestionTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } // Markup interface public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "ponder:Q{$id}:{$field}:{$hash}"; } public function getMarkupText($field) { return $this->getContent(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::getEngine(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } // votable interface public function getUserVoteEdgeType() { return PonderVotingUserHasQuestionEdgeType::EDGECONST; } public function getVotablePHID() { return $this->getPHID(); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getOriginalTitle() { // TODO: Make this actually save/return the original title. return $this->getTitle(); } public function getFullTitle() { $id = $this->getID(); $title = $this->getTitle(); return "Q{$id}: {$title}"; } /* -( 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->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht('The user who asked a question can always view and edit it.'); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getAuthorPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $answers = id(new PonderAnswer())->loadAllWhere( 'questionID = %d', $this->getID()); foreach ($answers as $answer) { $engine->destroyObject($answer); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } } diff --git a/src/applications/ponder/storage/PonderQuestionTransaction.php b/src/applications/ponder/storage/PonderQuestionTransaction.php index 0e4d07b150..38f2b5e45b 100644 --- a/src/applications/ponder/storage/PonderQuestionTransaction.php +++ b/src/applications/ponder/storage/PonderQuestionTransaction.php @@ -1,323 +1,321 @@ getTransactionType()) { case self::TYPE_ANSWERS: $phids[] = $this->getNewAnswerPHID(); $phids[] = $this->getObjectPHID(); break; } return $phids; } public function getRemarkupBlocks() { $blocks = parent::getRemarkupBlocks(); switch ($this->getTransactionType()) { case self::TYPE_CONTENT: $blocks[] = $this->getNewValue(); break; } return $blocks; } public function getTitle() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: if ($old === null) { return pht( '%s asked this question.', $this->renderHandleLink($author_phid)); } else { return pht( '%s edited the question title from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); } case self::TYPE_CONTENT: return pht( '%s edited the question description.', $this->renderHandleLink($author_phid)); case self::TYPE_ANSWERS: $answer_handle = $this->getHandle($this->getNewAnswerPHID()); $question_handle = $this->getHandle($object_phid); return pht( '%s answered %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case self::TYPE_STATUS: switch ($new) { case PonderQuestionStatus::STATUS_OPEN: return pht( '%s reopened this question.', $this->renderHandleLink($author_phid)); - case PonderQuestionStatus::STATUS_CLOSED: + case PonderQuestionStatus::STATUS_CLOSED_RESOLVED: + return pht( + '%s closed this question as resolved.', + $this->renderHandleLink($author_phid)); + case PonderQuestionStatus::STATUS_CLOSED_OBSOLETE: return pht( - '%s closed this question.', + '%s closed this question as obsolete.', + $this->renderHandleLink($author_phid)); + case PonderQuestionStatus::STATUS_CLOSED_DUPLICATE: + return pht( + '%s closed this question as a duplicate.', $this->renderHandleLink($author_phid)); } } return parent::getTitle(); } public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: case self::TYPE_CONTENT: return 'fa-pencil'; case self::TYPE_STATUS: - switch ($new) { - case PonderQuestionStatus::STATUS_OPEN: - return 'fa-check-circle'; - case PonderQuestionStatus::STATUS_CLOSED: - return 'fa-minus-circle'; - } + return PonderQuestionStatus::getQuestionStatusIcon($new); case self::TYPE_ANSWERS: return 'fa-plus'; } return parent::getIcon(); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: case self::TYPE_CONTENT: return PhabricatorTransactions::COLOR_BLUE; case self::TYPE_ANSWERS: return PhabricatorTransactions::COLOR_GREEN; case self::TYPE_STATUS: - switch ($new) { - case PonderQuestionStatus::STATUS_OPEN: - return PhabricatorTransactions::COLOR_GREEN; - case PonderQuestionStatus::STATUS_CLOSED: - return PhabricatorTransactions::COLOR_INDIGO; - } + return PonderQuestionStatus::getQuestionStatusTagColor($new); } } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_CONTENT: return true; } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } public function getActionStrength() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: if ($old === null) { return 3; } break; case self::TYPE_ANSWERS: return 2; } return parent::getActionStrength(); } public function getActionName() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: if ($old === null) { return pht('Asked'); } break; case self::TYPE_ANSWERS: return pht('Answered'); } return parent::getActionName(); } public function shouldHide() { switch ($this->getTransactionType()) { case self::TYPE_CONTENT: if ($this->getOldValue() === null) { return true; } else { return false; } break; } return parent::shouldHide(); } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: if ($old === null) { return pht( '%s asked a question: %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s edited the title of %s (was "%s")', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old); } case self::TYPE_CONTENT: return pht( '%s edited the description of %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case self::TYPE_ANSWERS: $answer_handle = $this->getHandle($this->getNewAnswerPHID()); $question_handle = $this->getHandle($object_phid); return pht( '%s answered %s', $this->renderHandleLink($author_phid), $answer_handle->renderLink($question_handle->getFullName())); case self::TYPE_STATUS: switch ($new) { case PonderQuestionStatus::STATUS_OPEN: return pht( '%s reopened %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PonderQuestionStatus::STATUS_CLOSED: return pht( '%s closed %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } } return parent::getTitleForFeed(); } public function getBodyForFeed(PhabricatorFeedStory $story) { $new = $this->getNewValue(); $old = $this->getOldValue(); $body = null; switch ($this->getTransactionType()) { case self::TYPE_TITLE: if ($old === null) { $question = $story->getObject($this->getObjectPHID()); return phutil_escape_html_newlines( id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(128) ->truncateString($question->getContent())); } break; case self::TYPE_ANSWERS: $answer = $this->getNewAnswerObject($story); if ($answer) { return phutil_escape_html_newlines( id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(128) ->truncateString($answer->getContent())); } break; } return parent::getBodyForFeed($story); } /** * Currently the application only supports adding answers one at a time. * This data is stored as a list of phids. Use this function to get the * new phid. */ private function getNewAnswerPHID() { $new = $this->getNewValue(); $old = $this->getOldValue(); $add = array_diff($new, $old); if (count($add) != 1) { throw new Exception( pht('There should be only one answer added at a time.')); } return reset($add); } /** * Generally, the answer object is only available if the transaction * type is `self::TYPE_ANSWERS`. * * Some stories - notably ones made before D7027 - will be of the more * generic @{class:PhabricatorApplicationTransactionFeedStory}. These * poor stories won't have the PonderAnswer loaded, and thus will have * less cool information. */ private function getNewAnswerObject(PhabricatorFeedStory $story) { if ($story instanceof PonderTransactionFeedStory) { $answer_phid = $this->getNewAnswerPHID(); if ($answer_phid) { return $story->getObject($answer_phid); } } return null; } }