diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php index d490a77c50..ebf09ec929 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php @@ -1,287 +1,294 @@ getViewer(); $id = $request->getURIData('id'); if ($id) { $poll = id(new PhabricatorSlowvoteQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$poll) { return new Aphront404Response(); } $is_new = false; } else { $poll = PhabricatorSlowvotePoll::initializeNewPoll($viewer); $is_new = true; } if ($is_new) { $v_projects = array(); } else { $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( $poll->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $v_projects = array_reverse($v_projects); } $e_question = true; $e_response = true; $errors = array(); $v_question = $poll->getQuestion(); $v_description = $poll->getDescription(); $v_responses = $poll->getResponseVisibility(); $v_shuffle = $poll->getShuffle(); $v_space = $poll->getSpacePHID(); $responses = $request->getArr('response'); if ($request->isFormPost()) { $v_question = $request->getStr('question'); $v_description = $request->getStr('description'); $v_responses = (int)$request->getInt('responses'); $v_shuffle = (int)$request->getBool('shuffle'); $v_view_policy = $request->getStr('viewPolicy'); $v_projects = $request->getArr('projects'); $v_space = $request->getStr('spacePHID'); if ($is_new) { $poll->setMethod($request->getInt('method')); } if (!strlen($v_question)) { $e_question = pht('Required'); $errors[] = pht('You must ask a poll question.'); } else { $e_question = null; } if ($is_new) { - $responses = array_filter($responses); + // NOTE: Make sure common and useful response "0" is preserved. + foreach ($responses as $key => $response) { + if (!strlen($response)) { + unset($responses[$key]); + } + } + if (empty($responses)) { $errors[] = pht('You must offer at least one response.'); $e_response = pht('Required'); } else { $e_response = null; } } $template = id(new PhabricatorSlowvoteTransaction()); $xactions = array(); if ($is_new) { $xactions[] = id(new PhabricatorSlowvoteTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_CREATE); } $xactions[] = id(clone $template) ->setTransactionType( PhabricatorSlowvoteQuestionTransaction::TRANSACTIONTYPE) ->setNewValue($v_question); $xactions[] = id(clone $template) ->setTransactionType( PhabricatorSlowvoteDescriptionTransaction::TRANSACTIONTYPE) ->setNewValue($v_description); $xactions[] = id(clone $template) ->setTransactionType( PhabricatorSlowvoteResponsesTransaction::TRANSACTIONTYPE) ->setNewValue($v_responses); $xactions[] = id(clone $template) ->setTransactionType( PhabricatorSlowvoteShuffleTransaction::TRANSACTIONTYPE) ->setNewValue($v_shuffle); $xactions[] = id(clone $template) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($v_view_policy); $xactions[] = id(clone $template) ->setTransactionType(PhabricatorTransactions::TYPE_SPACE) ->setNewValue($v_space); if (empty($errors)) { $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new PhabricatorSlowvoteTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $proj_edge_type) ->setNewValue(array('=' => array_fuse($v_projects))); $editor = id(new PhabricatorSlowvoteEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $xactions = $editor->applyTransactions($poll, $xactions); if ($is_new) { $poll->save(); foreach ($responses as $response) { $option = new PhabricatorSlowvoteOption(); $option->setName($response); $option->setPollID($poll->getID()); $option->save(); } } return id(new AphrontRedirectResponse()) - ->setURI('/V'.$poll->getID()); + ->setURI($poll->getURI()); } else { $poll->setViewPolicy($v_view_policy); } } $form = id(new AphrontFormView()) + ->setAction($request->getrequestURI()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Question')) ->setName('question') ->setValue($v_question) ->setError($e_question)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($viewer) ->setLabel(pht('Description')) ->setName('description') ->setValue($v_description)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Tags')) ->setName('projects') ->setValue($v_projects) ->setDatasource(new PhabricatorProjectDatasource())); if ($is_new) { for ($ii = 0; $ii < 10; $ii++) { $n = ($ii + 1); $response = id(new AphrontFormTextControl()) ->setLabel(pht('Response %d', $n)) ->setName('response[]') ->setValue(idx($responses, $ii, '')); if ($ii == 0) { $response->setError($e_response); } $form->appendChild($response); } } $poll_type_options = array( PhabricatorSlowvotePoll::METHOD_PLURALITY => pht('Plurality (Single Choice)'), PhabricatorSlowvotePoll::METHOD_APPROVAL => pht('Approval (Multiple Choice)'), ); $response_type_options = array( PhabricatorSlowvotePoll::RESPONSES_VISIBLE => pht('Allow anyone to see the responses'), PhabricatorSlowvotePoll::RESPONSES_VOTERS => pht('Require a vote to see the responses'), PhabricatorSlowvotePoll::RESPONSES_OWNER => pht('Only I can see the responses'), ); if ($is_new) { $form->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Vote Type')) ->setName('method') ->setValue($poll->getMethod()) ->setOptions($poll_type_options)); } else { $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Vote Type')) ->setValue(idx($poll_type_options, $poll->getMethod()))); } if ($is_new) { $title = pht('Create Slowvote'); $button = pht('Create'); $cancel_uri = $this->getApplicationURI(); $header_icon = 'fa-plus-square'; } else { $title = pht('Edit Poll: %s', $poll->getQuestion()); $button = pht('Save Changes'); $cancel_uri = '/V'.$poll->getID(); $header_icon = 'fa-pencil'; } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($poll) ->execute(); $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Responses')) ->setName('responses') ->setValue($v_responses) ->setOptions($response_type_options)) ->appendChild( id(new AphrontFormCheckboxControl()) ->setLabel(pht('Shuffle')) ->addCheckbox( 'shuffle', 1, pht('Show choices in random order.'), $v_shuffle)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setName('viewPolicy') ->setPolicyObject($poll) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setSpacePHID($v_space)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($button) ->addCancelButton($cancel_uri)); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title); $crumbs->setBorder(true); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Poll')) ->setFormErrors($errors) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setHeaderIcon($header_icon); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter($form_box); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild( array( $view, )); } } diff --git a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php index f788f5e94c..1ffab17791 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php @@ -1,158 +1,158 @@ getViewer(); $id = $request->getURIData('id'); $poll = id(new PhabricatorSlowvoteQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needOptions(true) ->needChoices(true) ->needViewerChoices(true) ->executeOne(); if (!$poll) { return new Aphront404Response(); } $poll_view = id(new SlowvoteEmbedView()) ->setUser($viewer) ->setPoll($poll); if ($request->isAjax()) { return id(new AphrontAjaxResponse()) ->setContent( array( 'pollID' => $poll->getID(), 'contentHTML' => $poll_view->render(), )); } - $header_icon = $poll->getIsClosed() ? 'fa-ban' : 'fa-circle-o'; + $header_icon = $poll->getIsClosed() ? 'fa-ban' : 'fa-square-o'; $header_name = $poll->getIsClosed() ? pht('Closed') : pht('Open'); - $header_color = $poll->getIsClosed() ? 'dark' : 'bluegrey'; + $header_color = $poll->getIsClosed() ? 'indigo' : 'bluegrey'; $header = id(new PHUIHeaderView()) ->setHeader($poll->getQuestion()) ->setUser($viewer) ->setStatus($header_icon, $header_color, $header_name) ->setPolicyObject($poll) ->setHeaderIcon('fa-bar-chart'); $curtain = $this->buildCurtain($poll); $subheader = $this->buildSubheaderView($poll); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb('V'.$poll->getID()); $crumbs->setBorder(true); $timeline = $this->buildTransactionTimeline( $poll, new PhabricatorSlowvoteTransactionQuery()); $add_comment = $this->buildCommentForm($poll); $poll_content = array( $poll_view, $timeline, $add_comment, ); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) ->setCurtain($curtain) ->setMainColumn($poll_content); return $this->newPage() ->setTitle('V'.$poll->getID().' '.$poll->getQuestion()) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($poll->getPHID())) ->appendChild($view); } private function buildCurtain(PhabricatorSlowvotePoll $poll) { $viewer = $this->getViewer(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $poll, PhabricatorPolicyCapability::CAN_EDIT); $curtain = $this->newCurtainView($poll); $is_closed = $poll->getIsClosed(); $close_poll_text = $is_closed ? pht('Reopen Poll') : pht('Close Poll'); - $close_poll_icon = $is_closed ? 'fa-play-circle-o' : 'fa-ban'; + $close_poll_icon = $is_closed ? 'fa-check' : 'fa-ban'; $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Poll')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI('edit/'.$poll->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $curtain->addAction( id(new PhabricatorActionView()) ->setName($close_poll_text) ->setIcon($close_poll_icon) ->setHref($this->getApplicationURI('close/'.$poll->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(true)); return $curtain; } private function buildSubheaderView( PhabricatorSlowvotePoll $poll) { $viewer = $this->getViewer(); $author = $viewer->renderHandle($poll->getAuthorPHID())->render(); $date = phabricator_datetime($poll->getDateCreated(), $viewer); $author = phutil_tag('strong', array(), $author); $person = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs(array($poll->getAuthorPHID())) ->needProfileImage(true) ->executeOne(); $image_uri = $person->getProfileImageURI(); $image_href = '/p/'.$person->getUsername(); $content = pht('Asked by %s on %s.', $author, $date); return id(new PHUIHeadThingView()) ->setImage($image_uri) ->setImageHref($image_href) ->setContent($content); } private function buildCommentForm(PhabricatorSlowvotePoll $poll) { $viewer = $this->getRequest()->getUser(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $add_comment_header = $is_serious ? pht('Add Comment') : pht('Enter Deliberations'); $draft = PhabricatorDraft::newFromUserAndKey($viewer, $poll->getPHID()); return id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($poll->getPHID()) ->setDraft($draft) ->setHeaderText($add_comment_header) ->setAction($this->getApplicationURI('/comment/'.$poll->getID().'/')) ->setSubmitButtonName(pht('Add Comment')); } } diff --git a/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php b/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php index 3f6baefd8d..38dbfb12d6 100644 --- a/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php +++ b/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php @@ -1,88 +1,96 @@ pht('Someone changes the poll details.'), PhabricatorSlowvoteTransaction::MAILTAG_RESPONSES => pht('Someone votes on a poll.'), PhabricatorSlowvoteTransaction::MAILTAG_OTHER => pht('Other poll activity not listed above occurs.'), ); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $monogram = $object->getMonogram(); $name = $object->getQuestion(); return id(new PhabricatorMetaMTAMail()) ->setSubject("{$monogram}: {$name}") ->addHeader('Thread-Topic', $monogram); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $description = $object->getDescription(); if (strlen($description)) { $body->addRemarkupSection( pht('SLOWVOTE DESCRIPTION'), $object->getDescription()); } $body->addLinkSection( pht('SLOWVOTE DETAIL'), PhabricatorEnv::getProductionURI('/'.$object->getMonogram())); return $body; } protected function getMailTo(PhabricatorLiskDAO $object) { return array( $object->getAuthorPHID(), $this->requireActor()->getPHID(), ); } protected function getMailSubjectPrefix() { return '[Slowvote]'; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new PhabricatorSlowvoteReplyHandler()) ->setMailReceiver($object); } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return true; } } diff --git a/src/applications/slowvote/query/PhabricatorSlowvoteSearchEngine.php b/src/applications/slowvote/query/PhabricatorSlowvoteSearchEngine.php index fee52a908c..d1db26b8a4 100644 --- a/src/applications/slowvote/query/PhabricatorSlowvoteSearchEngine.php +++ b/src/applications/slowvote/query/PhabricatorSlowvoteSearchEngine.php @@ -1,185 +1,186 @@ newQuery(); if ($map['voted']) { $query->withVotesByViewer(true); } if ($map['authorPHIDs']) { $query->withAuthorPHIDs($map['authorPHIDs']); } $statuses = $map['statuses']; if (count($statuses) == 1) { $status = head($statuses); if ($status == 'open') { $query->withIsClosed(false); } else { $query->withIsClosed(true); } } return $query; } protected function buildCustomSearchFields() { return array( id(new PhabricatorUsersSearchField()) ->setKey('authorPHIDs') ->setAliases(array('authors')) ->setLabel(pht('Authors')), id(new PhabricatorSearchCheckboxesField()) ->setKey('voted') ->setOptions(array( 'voted' => pht("Show only polls I've voted in."), )), id(new PhabricatorSearchCheckboxesField()) ->setKey('statuses') + ->setLabel(pht('Statuses')) ->setOptions(array( 'open' => pht('Open'), 'closed' => pht('Closed'), )), ); } protected function getURI($path) { return '/vote/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'open' => pht('Open Polls'), 'all' => pht('All Polls'), ); if ($this->requireViewer()->isLoggedIn()) { $names['authored'] = pht('Authored'); $names['voted'] = pht('Voted In'); } return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'open': return $query->setParameter('statuses', array('open')); case 'all': return $query; case 'authored': return $query->setParameter( 'authorPHIDs', array($this->requireViewer()->getPHID())); case 'voted': return $query->setParameter('voted', array('voted')); } return parent::buildSavedQueryFromBuiltin($query_key); } protected function getRequiredHandlePHIDsForResultList( array $polls, PhabricatorSavedQuery $query) { return mpull($polls, 'getAuthorPHID'); } protected function renderResultList( array $polls, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($polls, 'PhabricatorSlowvotePoll'); $viewer = $this->requireViewer(); $list = id(new PHUIObjectItemListView()) ->setUser($viewer); $phids = mpull($polls, 'getAuthorPHID'); foreach ($polls as $poll) { $date_created = phabricator_datetime($poll->getDateCreated(), $viewer); if ($poll->getAuthorPHID()) { $author = $handles[$poll->getAuthorPHID()]->renderLink(); } else { $author = null; } $item = id(new PHUIObjectItemView()) ->setUser($viewer) ->setObject($poll) ->setObjectName('V'.$poll->getID()) ->setHeader($poll->getQuestion()) ->setHref('/V'.$poll->getID()) ->addIcon('none', $date_created); if ($poll->getIsClosed()) { $item->setStatusIcon('fa-ban grey'); $item->setDisabled(true); } else { $item->setStatusIcon('fa-bar-chart'); } $description = $poll->getDescription(); if (strlen($description)) { $item->addAttribute(id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(120) ->truncateString($poll->getDescription())); } if ($author) { $item->addByline(pht('Author: %s', $author)); } $list->addItem($item); } $result = new PhabricatorApplicationSearchResultView(); $result->setObjectList($list); $result->setNoDataString(pht('No polls found.')); return $result; } protected function getNewUserBody() { $create_button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Create a Poll')) ->setHref('/vote/create/') ->setColor(PHUIButtonView::GREEN); $icon = $this->getApplication()->getIcon(); $app_name = $this->getApplication()->getName(); $view = id(new PHUIBigInfoView()) ->setIcon($icon) ->setTitle(pht('Welcome to %s', $app_name)) ->setDescription( pht('Poll other users to help facilitate decision making.')) ->addAction($create_button); return $view; } } diff --git a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php index 06c4f6f68f..3b642256d2 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php @@ -1,218 +1,222 @@ setViewer($actor) ->withClasses(array('PhabricatorSlowvoteApplication')) ->executeOne(); $view_policy = $app->getPolicy( PhabricatorSlowvoteDefaultViewCapability::CAPABILITY); return id(new PhabricatorSlowvotePoll()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setSpacePHID($actor->getDefaultSpacePHID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'question' => 'text255', 'responseVisibility' => 'uint32', 'shuffle' => 'bool', 'method' => 'uint32', 'description' => 'text', 'isClosed' => 'bool', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorSlowvotePollPHIDType::TYPECONST); } public function getOptions() { return $this->assertAttached($this->options); } public function attachOptions(array $options) { assert_instances_of($options, 'PhabricatorSlowvoteOption'); $this->options = $options; return $this; } public function getChoices() { return $this->assertAttached($this->choices); } public function attachChoices(array $choices) { assert_instances_of($choices, 'PhabricatorSlowvoteChoice'); $this->choices = $choices; return $this; } public function getViewerChoices(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->viewerChoices, $viewer->getPHID()); } public function attachViewerChoices(PhabricatorUser $viewer, array $choices) { if ($this->viewerChoices === self::ATTACHABLE) { $this->viewerChoices = array(); } assert_instances_of($choices, 'PhabricatorSlowvoteChoice'); $this->viewerChoices[$viewer->getPHID()] = $choices; return $this; } public function getMonogram() { return 'V'.$this->getID(); } + public function getURI() { + return '/'.$this->getMonogram(); + } + public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorSlowvoteEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorSlowvoteTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->viewPolicy; case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht('The author of a poll can always view and edit it.'); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getAuthorPHID()); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array($this->getAuthorPHID()); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $choices = id(new PhabricatorSlowvoteChoice())->loadAllWhere( 'pollID = %d', $this->getID()); foreach ($choices as $choice) { $choice->delete(); } $options = id(new PhabricatorSlowvoteOption())->loadAllWhere( 'pollID = %d', $this->getID()); foreach ($options as $option) { $option->delete(); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSpacesInterface )--------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } }