diff --git a/resources/sql/autopatches/20150828.ponder.wiki.1.sql b/resources/sql/autopatches/20150828.ponder.wiki.1.sql new file mode 100644 index 0000000000..dbb8334de4 --- /dev/null +++ b/resources/sql/autopatches/20150828.ponder.wiki.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_ponder.ponder_question + ADD answerWiki LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL; diff --git a/src/applications/ponder/controller/PonderQuestionEditController.php b/src/applications/ponder/controller/PonderQuestionEditController.php index 41123d6049..d31db7c157 100644 --- a/src/applications/ponder/controller/PonderQuestionEditController.php +++ b/src/applications/ponder/controller/PonderQuestionEditController.php @@ -1,188 +1,208 @@ 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); $is_new = false; } else { $is_new = true; $question = PonderQuestion::initializeNewQuestion($viewer); $v_projects = array(); } $v_title = $question->getTitle(); $v_content = $question->getContent(); + $v_wiki = $question->getAnswerWiki(); $v_view = $question->getViewPolicy(); $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_wiki = $request->getStr('answerWiki'); $v_projects = $request->getArr('projects'); $v_view = $request->getStr('viewPolicy'); $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_ANSWERWIKI) + ->setNewValue($v_wiki); + if (!$is_new) { $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_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')) + ->setLabel(pht('Question Details')) + ->setUser($viewer)) + ->appendChild( + id(new PhabricatorRemarkupControl()) + ->setUser($viewer) + ->setName('answerWiki') + ->setID('answerWiki') + ->setValue($v_wiki) + ->setLabel(pht('Answer Summary')) ->setUser($viewer)) ->appendControl( id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($question) ->setSpacePHID($v_space) ->setPolicies($policies) ->setValue($v_view) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)); if (!$is_new) { $form->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('Submit'))); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Question Preview')) ->setControlID('content') ->setPreviewURI($this->getApplicationURI('preview/')); + $answer_preview = id(new PHUIRemarkupPreviewPanel()) + ->setHeader(pht('Answer Summary Preview')) + ->setControlID('answerWiki') + ->setPreviewURI($this->getApplicationURI('preview/')); + $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, + $answer_preview, ), array( 'title' => $title, )); } } diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php index 3a4256c394..f7e70caab0 100644 --- a/src/applications/ponder/controller/PonderQuestionViewController.php +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -1,297 +1,307 @@ getViewer(); $id = $request->getURIData('id'); $question = id(new PonderQuestionQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needAnswers(true) ->needProjectPHIDs(true) ->executeOne(); if (!$question) { return new Aphront404Response(); } $answers = $this->buildAnswers($question); $answer_add_panel = id(new PonderAddAnswerView()) ->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 { $text = PonderQuestionStatus::getQuestionStatusFullName( $question->getStatus()); $icon = PonderQuestionStatus::getQuestionStatusIcon( $question->getStatus()); $header->setStatus($icon, 'dark', $text); } $actions = $this->buildActionListView($question); $properties = $this->buildPropertyListView($question, $actions); $sidebar = $this->buildSidebar($question); $content_id = celerity_generate_unique_node_id(); $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')); $comment_view = phutil_tag( 'div', array( 'id' => $content_id, 'style' => 'display: none;', ), array( $timeline, $add_comment, )); $footer = id(new PonderFooterView()) ->setContentID($content_id) ->setCount(count($xactions)); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties) ->appendChild($footer); if ($viewer->getPHID() == $question->getAuthorPHID()) { $status = $question->getStatus(); $answers_list = $question->getAnswers(); if ($answers_list && ($status == PonderQuestionStatus::STATUS_OPEN)) { $info_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->appendChild( pht( 'If this question has been resolved, please consider closing the question and marking the answer as helpful.')); $object_box->setInfoView($info_view); } } $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); $crumbs->addTextCrumb('Q'.$id, '/Q'.$id); + $answer_wiki = null; + if ($question->getAnswerWiki()) { + $answer = phutil_tag_div('mlt mlb msr msl', $question->getAnswerWiki()); + $answer_wiki = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Answer Summary')) + ->setColor(PHUIObjectBoxView::COLOR_BLUE) + ->appendChild($answer); + } + $ponder_view = id(new PHUITwoColumnView()) ->setMainColumn(array( $object_box, $comment_view, + $answer_wiki, $answers, $answer_add_panel, )) ->setSideColumn($sidebar) ->addClass('ponder-question-view'); return $this->buildApplicationPage( array( $crumbs, $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'; } else { $name = pht('Reopen Question'); $icon = 'fa-square-o'; } $view->addAction( id(new PhabricatorActionView()) ->setName($name) ->setIcon($icon) ->setWorkflow(true) ->setDisabled(!$can_edit) ->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(); $details = PhabricatorMarkupEngine::renderOneObject( $question, $question->getMarkupField(), $viewer); if ($details) { $view->addSectionHeader( pht('Details'), PHUIPropertyListView::ICON_SUMMARY); $view->addTextContent( array( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $details), )); } return $view; } /** * 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(PonderQuestion $question) { $viewer = $this->getViewer(); $answers = $question->getAnswers(); $author_phids = mpull($answers, 'getAuthorPHID'); $handles = $this->loadViewerHandles($author_phids); $answers_sort = array_reverse(msort($answers, 'getVoteCount')); $view = array(); foreach ($answers_sort as $answer) { $id = $answer->getID(); $handle = $handles[$answer->getAuthorPHID()]; $timeline = $this->buildTransactionTimeline( $answer, id(new PonderAnswerTransactionQuery()) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))); $xactions = $timeline->getTransactions(); $view[] = id(new PonderAnswerView()) ->setUser($viewer) ->setAnswer($answer) ->setTransactions($xactions) ->setTimeline($timeline) ->setHandle($handle); } return $view; } private function buildSidebar(PonderQuestion $question) { $viewer = $this->getViewer(); $status = $question->getStatus(); $id = $question->getID(); $questions = id(new PonderQuestionQuery()) ->setViewer($viewer) ->withStatuses(array($status)) ->withEdgeLogicPHIDs( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, PhabricatorQueryConstraint::OPERATOR_OR, $question->getProjectPHIDs()) ->setLimit(10) ->execute(); $list = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setNoDataString(pht('No similar questions found.')); foreach ($questions as $question) { if ($id == $question->getID()) { continue; } $item = new PHUIObjectItemView(); $item->setObjectName('Q'.$question->getID()); $item->setHeader($question->getTitle()); $item->setHref('/Q'.$question->getID()); $item->setObject($question); $item->addAttribute( pht('%d Answer(s)', $question->getAnswerCount())); $list->addItem($item); } $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Similar Questions')) ->setObjectList($list); return $box; } } diff --git a/src/applications/ponder/editor/PonderQuestionEditor.php b/src/applications/ponder/editor/PonderQuestionEditor.php index c2d9ef231b..b2f143b176 100644 --- a/src/applications/ponder/editor/PonderQuestionEditor.php +++ b/src/applications/ponder/editor/PonderQuestionEditor.php @@ -1,285 +1,293 @@ answer = $answer; return $this; } private function getAnswer() { return $this->answer; } protected function shouldApplyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PonderQuestionTransaction::TYPE_ANSWERS: return true; } } return false; } protected function applyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PonderQuestionTransaction::TYPE_ANSWERS: $new_value = $xaction->getNewValue(); $new = idx($new_value, '+', array()); foreach ($new as $new_answer) { $answer = idx($new_answer, 'answer'); if (!$answer) { continue; } $answer->save(); $this->setAnswer($answer); } break; } } } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; $types[] = PhabricatorTransactions::TYPE_SPACE; $types[] = PonderQuestionTransaction::TYPE_TITLE; $types[] = PonderQuestionTransaction::TYPE_CONTENT; $types[] = PonderQuestionTransaction::TYPE_ANSWERS; $types[] = PonderQuestionTransaction::TYPE_STATUS; + $types[] = PonderQuestionTransaction::TYPE_ANSWERWIKI; return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PonderQuestionTransaction::TYPE_TITLE: return $object->getTitle(); case PonderQuestionTransaction::TYPE_CONTENT: return $object->getContent(); case PonderQuestionTransaction::TYPE_ANSWERS: return mpull($object->getAnswers(), 'getPHID'); case PonderQuestionTransaction::TYPE_STATUS: return $object->getStatus(); + case PonderQuestionTransaction::TYPE_ANSWERWIKI: + return $object->getAnswerWiki(); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PonderQuestionTransaction::TYPE_TITLE: case PonderQuestionTransaction::TYPE_CONTENT: case PonderQuestionTransaction::TYPE_STATUS: + case PonderQuestionTransaction::TYPE_ANSWERWIKI: return $xaction->getNewValue(); case PonderQuestionTransaction::TYPE_ANSWERS: $raw_new_value = $xaction->getNewValue(); $new_value = array(); foreach ($raw_new_value as $key => $answers) { $phids = array(); foreach ($answers as $answer) { $obj = idx($answer, 'answer'); if (!$answer) { continue; } $phids[] = $obj->getPHID(); } $new_value[$key] = $phids; } $xaction->setNewValue($new_value); return $this->getPHIDTransactionNewValue($xaction); } } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PonderQuestionTransaction::TYPE_TITLE: $object->setTitle($xaction->getNewValue()); break; case PonderQuestionTransaction::TYPE_CONTENT: $object->setContent($xaction->getNewValue()); break; case PonderQuestionTransaction::TYPE_STATUS: $object->setStatus($xaction->getNewValue()); break; + case PonderQuestionTransaction::TYPE_ANSWERWIKI: + $object->setAnswerWiki($xaction->getNewValue()); + break; case PonderQuestionTransaction::TYPE_ANSWERS: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $add = array_diff_key($new, $old); $rem = array_diff_key($old, $new); $count = $object->getAnswerCount(); $count += count($add); $count -= count($rem); $object->setAnswerCount($count); break; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return; } protected function mergeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $type = $u->getTransactionType(); switch ($type) { case PonderQuestionTransaction::TYPE_TITLE: case PonderQuestionTransaction::TYPE_CONTENT: case PonderQuestionTransaction::TYPE_STATUS: + case PonderQuestionTransaction::TYPE_ANSWERWIKI: return $v; } return parent::mergeTransactions($u, $v); } protected function supportsSearch() { return true; } protected function shouldImplyCC( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PonderQuestionTransaction::TYPE_ANSWERS: return false; } return parent::shouldImplyCC($object, $xaction); } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PonderQuestionTransaction::TYPE_ANSWERS: return false; } } return true; } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PonderQuestionTransaction::TYPE_ANSWERS: return false; } } return true; } public function getMailTagsMap() { return array( PonderQuestionTransaction::MAILTAG_DETAILS => pht('Someone changes the questions details.'), PonderQuestionTransaction::MAILTAG_ANSWERS => pht('Someone adds a new answer.'), PonderQuestionTransaction::MAILTAG_COMMENT => pht('Someone comments on the question.'), PonderQuestionTransaction::MAILTAG_OTHER => pht('Other question activity not listed above occurs.'), ); } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new PonderQuestionReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $title = $object->getTitle(); $original_title = $object->getOriginalTitle(); return id(new PhabricatorMetaMTAMail()) ->setSubject("Q{$id}: {$title}") ->addHeader('Thread-Topic', "Q{$id}: {$original_title}"); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $header = pht('QUESTION DETAIL'); $uri = '/Q'.$object->getID(); foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); // If the user just asked the question, add the question text. if ($type == PonderQuestionTransaction::TYPE_CONTENT) { if ($old === null) { $body->addRawSection($new); } } } $body->addLinkSection( $header, PhabricatorEnv::getProductionURI($uri)); return $body; } protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { return id(new HeraldPonderQuestionAdapter()) ->setQuestion($object); } } diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php index bc71690387..f02823b273 100644 --- a/src/applications/ponder/storage/PonderQuestion.php +++ b/src/applications/ponder/storage/PonderQuestion.php @@ -1,292 +1,294 @@ setViewer($actor) ->withClasses(array('PhabricatorPonderApplication')) ->executeOne(); $view_policy = $app->getPolicy( PonderDefaultViewCapability::CAPABILITY); return id(new PonderQuestion()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setStatus(PonderQuestionStatus::STATUS_OPEN) ->setAnswerCount(0) ->setSpacePHID($actor->getDefaultSpacePHID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'status' => 'text32', 'content' => 'text', + 'answerWiki' => 'text', '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'), ), '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 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(); } 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: $app = PhabricatorApplication::getByClass( 'PhabricatorPonderApplication'); return $app->getPolicy(PonderModerateCapability::CAPABILITY); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { if (PhabricatorPolicyFilter::hasCapability( $viewer, $this, PhabricatorPolicyCapability::CAN_EDIT)) { return true; } } return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { $out = array(); $out[] = pht('The user who asked a question can always view and edit it.'); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $out[] = pht( 'A moderator can always view the question.'); break; } return $out; } /* -( 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 7b7188b448..cb90cc70a4 100644 --- a/src/applications/ponder/storage/PonderQuestionTransaction.php +++ b/src/applications/ponder/storage/PonderQuestionTransaction.php @@ -1,339 +1,353 @@ 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_ANSWERWIKI: + return pht( + '%s edited the question answer wiki.', + $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_RESOLVED: return pht( '%s closed this question as resolved.', $this->renderHandleLink($author_phid)); case PonderQuestionStatus::STATUS_CLOSED_OBSOLETE: return pht( '%s closed this question as obsolete.', $this->renderHandleLink($author_phid)); case PonderQuestionStatus::STATUS_CLOSED_INVALID: return pht( '%s closed this question as invalid.', $this->renderHandleLink($author_phid)); } } return parent::getTitle(); } public function getMailTags() { $tags = parent::getMailTags(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $tags[] = self::MAILTAG_COMMENT; break; case self::TYPE_TITLE: case self::TYPE_CONTENT: case self::TYPE_STATUS: + case self::TYPE_ANSWERWIKI: $tags[] = self::MAILTAG_DETAILS; break; case self::TYPE_ANSWERS: $tags[] = self::MAILTAG_ANSWERS; break; default: $tags[] = self::MAILTAG_OTHER; break; } return $tags; } public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: case self::TYPE_CONTENT: + case self::TYPE_ANSWERWIKI: return 'fa-pencil'; case self::TYPE_STATUS: 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: + case self::TYPE_ANSWERWIKI: return PhabricatorTransactions::COLOR_BLUE; case self::TYPE_ANSWERS: return PhabricatorTransactions::COLOR_GREEN; case self::TYPE_STATUS: return PonderQuestionStatus::getQuestionStatusTagColor($new); } } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_CONTENT: + case self::TYPE_ANSWERWIKI: 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_ANSWERWIKI: + return pht( + '%s edited the answer wiki for %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_RESOLVED: return pht( '%s closed %s as resolved.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PonderQuestionStatus::STATUS_CLOSED_INVALID: return pht( '%s closed %s as invalid.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PonderQuestionStatus::STATUS_CLOSED_OBSOLETE: return pht( '%s closed %s as obsolete.', $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); } }