diff --git a/resources/sql/autopatches/20150806.ponder.status.1.sql b/resources/sql/autopatches/20150806.ponder.status.1.sql new file mode 100644 --- /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 --- /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 --- /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 --- a/src/applications/ponder/application/PhabricatorPonderApplication.php +++ b/src/applications/ponder/application/PhabricatorPonderApplication.php @@ -71,7 +71,7 @@ => 'PonderQuestionHistoryController', 'preview/' => 'PhabricatorMarkupPreviewController', - 'question/(?Popen|close)/(?P[1-9]\d*)/' + 'question/status/(?P[1-9]\d*)/' => 'PonderQuestionStatusController', 'vote/' => 'PonderVoteSaveController', ), diff --git a/src/applications/ponder/constants/PonderQuestionStatus.php b/src/applications/ponder/constants/PonderQuestionStatus.php --- a/src/applications/ponder/constants/PonderQuestionStatus.php +++ b/src/applications/ponder/constants/PonderQuestionStatus.php @@ -2,20 +2,40 @@ final class PonderQuestionStatus extends PonderConstants { - const STATUS_OPEN = 0; - const STATUS_CLOSED = 1; + const STATUS_OPEN = 'open'; + const STATUS_CLOSED_RESOLVED = 'resolved'; + const STATUS_CLOSED_OBSOLETE = 'obsolete'; + const STATUS_CLOSED_DUPLICATE = 'duplicate'; public static function getQuestionStatusMap() { return array( - self::STATUS_OPEN => 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')); } @@ -23,7 +43,9 @@ 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); @@ -32,10 +54,27 @@ 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 --- a/src/applications/ponder/controller/PonderQuestionEditController.php +++ b/src/applications/ponder/controller/PonderQuestionEditController.php @@ -33,6 +33,8 @@ $v_view = $question->getViewPolicy(); $v_edit = $question->getEditPolicy(); $v_space = $question->getSpacePHID(); + $v_status = $question->getStatus(); + $errors = array(); $e_title = true; @@ -43,6 +45,7 @@ $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) { @@ -66,6 +69,10 @@ ->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); @@ -130,7 +137,13 @@ ->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()) @@ -149,21 +162,23 @@ ->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, @@ -171,7 +186,7 @@ $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 --- a/src/applications/ponder/controller/PonderQuestionStatusController.php +++ b/src/applications/ponder/controller/PonderQuestionStatusController.php @@ -6,7 +6,6 @@ public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $id = $request->getURIData('id'); - $status = $request->getURIData('status'); $question = id(new PonderQuestionQuery()) ->setViewer($viewer) @@ -21,29 +20,46 @@ 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 --- a/src/applications/ponder/controller/PonderQuestionViewController.php +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -45,7 +45,11 @@ 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); @@ -109,21 +113,18 @@ 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()) diff --git a/src/applications/ponder/query/PonderQuestionQuery.php b/src/applications/ponder/query/PonderQuestionQuery.php --- a/src/applications/ponder/query/PonderQuestionQuery.php +++ b/src/applications/ponder/query/PonderQuestionQuery.php @@ -5,17 +5,12 @@ private $ids; private $phids; + private $status; private $authorPHIDs; private $answererPHIDs; private $needProjectPHIDs; - private $status = 'status-any'; - - const STATUS_ANY = 'status-any'; - const STATUS_OPEN = 'status-open'; - const STATUS_CLOSED = 'status-closed'; - private $needAnswers; private $needViewerVotes; @@ -34,7 +29,7 @@ return $this; } - public function withStatus($status) { + public function withStatuses($status) { $this->status = $status; return $this; } @@ -84,24 +79,10 @@ } 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; diff --git a/src/applications/ponder/query/PonderQuestionSearchEngine.php b/src/applications/ponder/query/PonderQuestionSearchEngine.php --- a/src/applications/ponder/query/PonderQuestionSearchEngine.php +++ b/src/applications/ponder/query/PonderQuestionSearchEngine.php @@ -27,16 +27,8 @@ $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; @@ -52,9 +44,9 @@ ->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()), ); } @@ -66,6 +58,7 @@ protected function getBuiltinQueryNames() { $names = array( 'open' => pht('Open Questions'), + 'resolved' => pht('Resolved Questions'), 'all' => pht('All Questions'), ); @@ -85,7 +78,11 @@ 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', diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php --- a/src/applications/ponder/storage/PonderQuestion.php +++ b/src/applications/ponder/storage/PonderQuestion.php @@ -65,7 +65,7 @@ self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'voteCount' => 'sint32', - 'status' => 'uint32', + 'status' => 'text32', 'content' => 'text', 'heat' => 'double', 'answerCount' => 'uint32', diff --git a/src/applications/ponder/storage/PonderQuestionTransaction.php b/src/applications/ponder/storage/PonderQuestionTransaction.php --- a/src/applications/ponder/storage/PonderQuestionTransaction.php +++ b/src/applications/ponder/storage/PonderQuestionTransaction.php @@ -87,9 +87,17 @@ 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)); } } @@ -106,12 +114,7 @@ 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'; } @@ -130,12 +133,7 @@ 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); } }