From 57097c2874e7995fb2960839b2eb01f8c867d422 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 8 Jul 2011 11:13:11 -0700 Subject: [PATCH] Port the "Slowvote" application Summary: Port slowvote. This has some style/layout roughness but gets us most of the way there. I'll followup to fix some of the markup issues. Test Plan: Created and voted in several different kinds of poll. Reviewed By: codeblock Reviewers: codeblock, tomo, jungejason, aran, tuomaspelkonen Commenters: aran, jungejason CC: aran, codeblock, jungejason, epriestley Differential Revision: 613 --- resources/sql/patches/056.slowvote.sql | 50 +++ src/__celerity_resource_map__.php | 31 +- src/__phutil_library_map__.php | 18 + ...AphrontDefaultApplicationConfiguration.php | 6 + .../constants/PhabricatorPHIDConstants.php | 2 + .../base/PhabricatorSlowvoteController.php | 45 ++ .../slowvote/controller/base/__init__.php | 16 + .../PhabricatorSlowvoteCreateController.php | 160 +++++++ .../slowvote/controller/create/__init__.php | 24 ++ .../PhabricatorSlowvoteListController.php | 167 ++++++++ .../slowvote/controller/list/__init__.php | 24 ++ .../PhabricatorSlowvotePollController.php | 390 ++++++++++++++++++ .../slowvote/controller/poll/__init__.php | 31 ++ .../storage/base/PhabricatorSlowvoteDAO.php | 25 ++ .../slowvote/storage/base/__init__.php | 12 + .../choice/PhabricatorSlowvoteChoice.php | 25 ++ .../slowvote/storage/choice/__init__.php | 12 + .../comment/PhabricatorSlowvoteComment.php | 25 ++ .../slowvote/storage/comment/__init__.php | 12 + .../option/PhabricatorSlowvoteOption.php | 25 ++ .../slowvote/storage/option/__init__.php | 12 + .../storage/poll/PhabricatorSlowvotePoll.php | 46 +++ .../slowvote/storage/poll/__init__.php | 14 + src/docs/userguide/slowvote.diviner | 27 ++ .../css/application/slowvote/slowvote.css | 60 +++ 25 files changed, 1248 insertions(+), 11 deletions(-) create mode 100644 resources/sql/patches/056.slowvote.sql create mode 100644 src/applications/slowvote/controller/base/PhabricatorSlowvoteController.php create mode 100644 src/applications/slowvote/controller/base/__init__.php create mode 100644 src/applications/slowvote/controller/create/PhabricatorSlowvoteCreateController.php create mode 100644 src/applications/slowvote/controller/create/__init__.php create mode 100644 src/applications/slowvote/controller/list/PhabricatorSlowvoteListController.php create mode 100644 src/applications/slowvote/controller/list/__init__.php create mode 100644 src/applications/slowvote/controller/poll/PhabricatorSlowvotePollController.php create mode 100644 src/applications/slowvote/controller/poll/__init__.php create mode 100644 src/applications/slowvote/storage/base/PhabricatorSlowvoteDAO.php create mode 100644 src/applications/slowvote/storage/base/__init__.php create mode 100644 src/applications/slowvote/storage/choice/PhabricatorSlowvoteChoice.php create mode 100644 src/applications/slowvote/storage/choice/__init__.php create mode 100644 src/applications/slowvote/storage/comment/PhabricatorSlowvoteComment.php create mode 100644 src/applications/slowvote/storage/comment/__init__.php create mode 100644 src/applications/slowvote/storage/option/PhabricatorSlowvoteOption.php create mode 100644 src/applications/slowvote/storage/option/__init__.php create mode 100644 src/applications/slowvote/storage/poll/PhabricatorSlowvotePoll.php create mode 100644 src/applications/slowvote/storage/poll/__init__.php create mode 100644 src/docs/userguide/slowvote.diviner create mode 100644 webroot/rsrc/css/application/slowvote/slowvote.css diff --git a/resources/sql/patches/056.slowvote.sql b/resources/sql/patches/056.slowvote.sql new file mode 100644 index 0000000000..dd877deba9 --- /dev/null +++ b/resources/sql/patches/056.slowvote.sql @@ -0,0 +1,50 @@ +CREATE DATABASE phabricator_slowvote; + +CREATE TABLE phabricator_slowvote.slowvote_poll ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + question VARCHAR(255) NOT NULL, + phid VARCHAR(64) BINARY NOT NULL, + UNIQUE KEY (phid), + authorPHID VARCHAR(64) BINARY NOT NULL, + responseVisibility INT UNSIGNED NOT NULL, + shuffle INT UNSIGNED NOT NULL, + method INT UNSIGNED NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +); + +CREATE TABLE phabricator_slowvote.slowvote_option ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + pollID INT UNSIGNED NOT NULL, + KEY (pollID), + name VARCHAR(255) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +); + +CREATE TABLE phabricator_slowvote.slowvote_comment ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + pollID INT UNSIGNED NOT NULL, + UNIQUE KEY (pollID, authorPHID), + authorPHID VARCHAR(64) BINARY NOT NULL, + commentText LONGBLOB NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +); + +CREATE TABLE phabricator_slowvote.slowvote_choice ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + pollID INT UNSIGNED NOT NULL, + KEY (pollID), + optionID INT UNSIGNED NOT NULL, + authorPHID VARCHAR(64) BINARY NOT NULL, + KEY (authorPHID), + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +); + +INSERT INTO phabricator_directory.directory_item + (name, description, href, categoryID, sequence, dateCreated, dateModified) +VALUES + ("Slowvote", "Design by committee.", "/vote/", 5, 250, + UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); \ No newline at end of file diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 5b5077043d..dbd9056ddf 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -283,17 +283,6 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/application/herald/herald-test.css', ), - 0 => - array( - 'uri' => '/res/1da00bfe/rsrc/js/javelin/lib/__tests__/URI.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-uri', - 1 => 'javelin-php-serializer', - ), - 'disk' => '/rsrc/js/javelin/lib/__tests__/URI.js', - ), 'javelin-behavior' => array( 'uri' => '/res/b28adfa1/rsrc/js/javelin/lib/behavior.js', @@ -487,6 +476,17 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/differential/behavior-show-all-comments.js', ), + 0 => + array( + 'uri' => '/res/1da00bfe/rsrc/js/javelin/lib/__tests__/URI.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-uri', + 1 => 'javelin-php-serializer', + ), + 'disk' => '/rsrc/js/javelin/lib/__tests__/URI.js', + ), 'javelin-behavior-differential-show-more' => array( 'uri' => '/res/a766c717/rsrc/js/application/differential/behavior-show-more.js', @@ -1099,6 +1099,15 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/core/ShapedRequest.js', ), + 'phabricator-slowvote-css' => + array( + 'uri' => '/res/2caefc66/rsrc/css/application/slowvote/slowvote.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/slowvote/slowvote.css', + ), 'phabricator-standard-page-view' => array( 'uri' => '/res/e8238633/rsrc/css/application/base/standard-page-view.css', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index dbfcc996fd..97d1b3adcc 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -521,6 +521,15 @@ phutil_register_library_map(array( 'PhabricatorSearchSelectController' => 'applications/search/controller/select', 'PhabricatorSearchUserIndexer' => 'applications/search/index/indexer/user', 'PhabricatorSetup' => 'infrastructure/setup', + 'PhabricatorSlowvoteChoice' => 'applications/slowvote/storage/choice', + 'PhabricatorSlowvoteComment' => 'applications/slowvote/storage/comment', + 'PhabricatorSlowvoteController' => 'applications/slowvote/controller/base', + 'PhabricatorSlowvoteCreateController' => 'applications/slowvote/controller/create', + 'PhabricatorSlowvoteDAO' => 'applications/slowvote/storage/base', + 'PhabricatorSlowvoteListController' => 'applications/slowvote/controller/list', + 'PhabricatorSlowvoteOption' => 'applications/slowvote/storage/option', + 'PhabricatorSlowvotePoll' => 'applications/slowvote/storage/poll', + 'PhabricatorSlowvotePollController' => 'applications/slowvote/controller/poll', 'PhabricatorStandardPageView' => 'view/page/standard', 'PhabricatorStatusController' => 'applications/status/base', 'PhabricatorSyntaxHighlighter' => 'applications/markup/syntax', @@ -1002,6 +1011,15 @@ phutil_register_library_map(array( 'PhabricatorSearchResultView' => 'AphrontView', 'PhabricatorSearchSelectController' => 'PhabricatorSearchController', 'PhabricatorSearchUserIndexer' => 'PhabricatorSearchDocumentIndexer', + 'PhabricatorSlowvoteChoice' => 'PhabricatorSlowvoteDAO', + 'PhabricatorSlowvoteComment' => 'PhabricatorSlowvoteDAO', + 'PhabricatorSlowvoteController' => 'PhabricatorController', + 'PhabricatorSlowvoteCreateController' => 'PhabricatorSlowvoteController', + 'PhabricatorSlowvoteDAO' => 'PhabricatorLiskDAO', + 'PhabricatorSlowvoteListController' => 'PhabricatorSlowvoteController', + 'PhabricatorSlowvoteOption' => 'PhabricatorSlowvoteDAO', + 'PhabricatorSlowvotePoll' => 'PhabricatorSlowvoteDAO', + 'PhabricatorSlowvotePollController' => 'PhabricatorSlowvoteController', 'PhabricatorStandardPageView' => 'AphrontPageView', 'PhabricatorStatusController' => 'PhabricatorController', 'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index a36de405d8..0269123922 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -334,6 +334,12 @@ class AphrontDefaultApplicationConfiguration '/feed/' => array( '$' => 'PhabricatorFeedStreamController', ), + + '/V(?P\d+)$' => 'PhabricatorSlowvotePollController', + '/vote/' => array( + '(?:view/(?P\w+)/)?$' => 'PhabricatorSlowvoteListController', + 'create/' => 'PhabricatorSlowvoteCreateController', + ), ); } diff --git a/src/applications/phid/constants/PhabricatorPHIDConstants.php b/src/applications/phid/constants/PhabricatorPHIDConstants.php index 87703b4079..ffb0a84a85 100644 --- a/src/applications/phid/constants/PhabricatorPHIDConstants.php +++ b/src/applications/phid/constants/PhabricatorPHIDConstants.php @@ -31,6 +31,7 @@ final class PhabricatorPHIDConstants { const PHID_TYPE_OPKG = 'OPKG'; const PHID_TYPE_PSTE = 'PSTE'; const PHID_TYPE_STRY = 'STRY'; + const PHID_TYPE_POLL = 'POLL'; public static function getTypes() { return array( @@ -47,6 +48,7 @@ final class PhabricatorPHIDConstants { self::PHID_TYPE_PSTE, self::PHID_TYPE_OPKG, self::PHID_TYPE_STRY, + self::PHID_TYPE_POLL, ); } } diff --git a/src/applications/slowvote/controller/base/PhabricatorSlowvoteController.php b/src/applications/slowvote/controller/base/PhabricatorSlowvoteController.php new file mode 100644 index 0000000000..c87558a916 --- /dev/null +++ b/src/applications/slowvote/controller/base/PhabricatorSlowvoteController.php @@ -0,0 +1,45 @@ +buildStandardPageView(); + + $page->setApplicationName('Slowvote'); + $page->setBaseURI('/vote/'); + $page->setTitle(idx($data, 'title')); + $page->setGlyph("\xE2\x9C\x94"); + + $doc_href = PhabricatorEnv::getDoclink('articles/Slowvote_User_Guide.html'); + $page->setTabs( + array( + 'help' => array( + 'name' => 'Help', + 'href' => $doc_href, + ), + ), + null); + + $page->appendChild($view); + + $response = new AphrontWebpageResponse(); + return $response->setContent($page->render()); + } + +} diff --git a/src/applications/slowvote/controller/base/__init__.php b/src/applications/slowvote/controller/base/__init__.php new file mode 100644 index 0000000000..71b6091fdc --- /dev/null +++ b/src/applications/slowvote/controller/base/__init__.php @@ -0,0 +1,16 @@ +getRequest(); + $user = $request->getUser(); + + $poll = new PhabricatorSlowvotePoll(); + $poll->setAuthorPHID($user->getPHID()); + + $e_question = true; + $e_response = true; + $errors = array(); + + $responses = $request->getArr('response'); + + if ($request->isFormPost()) { + $poll->setQuestion($request->getStr('question')); + $poll->setResponseVisibility($request->getInt('response_visibility')); + $poll->setShuffle($request->getBool('shuffle', false)); + $poll->setMethod($request->getInt('method')); + + if (!strlen($poll->getQuestion())) { + $e_question = 'Required'; + $errors[] = 'You must ask a poll question.'; + } else { + $e_question = null; + } + + $responses = array_filter($responses); + if (empty($responses)) { + $errors[] = 'You must offer at least one response.'; + $e_response = 'Required'; + } else { + $e_response = null; + } + + if (empty($errors)) { + $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()); + } + } + + $error_view = null; + if ($errors) { + $error_view = new AphrontErrorView(); + $error_view->setTitle('Form Errors'); + $error_view->setErrors($errors); + } + + $form = id(new AphrontFormView()) + ->setUser($user) + ->appendChild( + '

Resolve issues and build '. + 'consensus through protracted deliberation.

') + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Question') + ->setName('question') + ->setValue($poll->getQuestion()) + ->setError($e_question)); + + for ($ii = 0; $ii < 10; $ii++) { + $n = ($ii + 1); + $response = id(new AphrontFormTextControl()) + ->setLabel("Response {$n}") + ->setName('response[]') + ->setValue(idx($responses, $ii, '')); + + if ($ii == 0) { + $response->setError($e_response); + } + + $form->appendChild($response); + } + + $poll_type_options = array( + PhabricatorSlowvotePoll::METHOD_PLURALITY => 'Plurality (Single Choice)', + PhabricatorSlowvotePoll::METHOD_APPROVAL => 'Approval (Multiple Choice)', + ); + + $response_type_options = array( + PhabricatorSlowvotePoll::RESPONSES_VISIBLE + => 'Allow anyone to see the responses', + PhabricatorSlowvotePoll::RESPONSES_VOTERS + => 'Require a vote to see the responses', + PhabricatorSlowvotePoll::RESPONSES_OWNER + => 'Only I can see the responses', + ); + + $form + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel('Vote Type') + ->setName('method') + ->setValue($poll->getMethod()) + ->setOptions($poll_type_options)) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel('Responses') + ->setName('response_visibility') + ->setValue($poll->getResponseVisibility()) + ->setOptions($response_type_options)) + ->appendChild( + id(new AphrontFormCheckboxControl()) + ->setLabel('Shuffle') + ->addCheckbox( + 'shuffle', + 1, + 'Show choices in random order', + $poll->getShuffle())) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Create Slowvote') + ->addCancelButton('/vote/')); + + $panel = new AphrontPanelView(); + $panel->setWidth(AphrontPanelView::WIDTH_FORM); + $panel->setHeader('Create Slowvote'); + $panel->appendChild($form); + + return $this->buildStandardPageResponse( + array( + $error_view, + $panel, + ), + array( + 'title' => 'Create Slowvote', + )); + } + +} diff --git a/src/applications/slowvote/controller/create/__init__.php b/src/applications/slowvote/controller/create/__init__.php new file mode 100644 index 0000000000..8a0600d32c --- /dev/null +++ b/src/applications/slowvote/controller/create/__init__.php @@ -0,0 +1,24 @@ +view = idx($data, 'view'); + } + + public function processRequest() { + + $request = $this->getRequest(); + $user = $request->getUser(); + + $filters = array( + self::VIEW_ALL => 'All Slowvotes', + self::VIEW_CREATED => 'Created', + self::VIEW_VOTED => 'Voted In', + ); + + $view = isset($filters[$this->view]) + ? $this->view + : self::VIEW_ALL; + + $side_nav = new AphrontSideNavView(); + foreach ($filters as $key => $name) { + $side_nav->addNavItem( + phutil_render_tag( + 'a', + array( + 'href' => '/vote/view/'.$key.'/', + 'class' => ($view == $key) ? 'aphront-side-nav-selected' : null, + ), + phutil_escape_html($name))); + } + + + $poll = new PhabricatorSlowvotePoll(); + $conn = $poll->establishConnection('r'); + + $pager = new AphrontPagerView(); + $pager->setOffset($request->getInt('page')); + $pager->setURI($request->getRequestURI(), 'page'); + $offset = $pager->getOffset(); + $limit = $pager->getPageSize() + 1; + + switch ($view) { + case self::VIEW_ALL: + $data = queryfx_all( + $conn, + 'SELECT * FROM %T ORDER BY id DESC LIMIT %d, %d', + $poll->getTableName(), + $offset, + $limit); + break; + case self::VIEW_CREATED: + $data = queryfx_all( + $conn, + 'SELECT * FROM %T WHERE authorPHID = %s ORDER BY id DESC + LIMIT %d, %d', + $poll->getTableName(), + $user->getPHID(), + $offset, + $limit); + break; + case self::VIEW_VOTED: + $choice = new PhabricatorSlowvoteChoice(); + $data = queryfx_all( + $conn, + 'SELECT p.* FROM %T p JOIN %T o + ON o.pollID = p.id + WHERE o.authorPHID = %s + GROUP BY p.id + ORDER BY p.id DESC + LIMIT %d, %d', + $poll->getTableName(), + $choice->getTableName(), + $user->getPHID(), + $offset, + $limit); + break; + } + + $data = $pager->sliceResults($data); + $polls = $poll->loadAllFromArray($data); + + $phids = mpull($polls, 'getAuthorPHID'); + $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); + + $rows = array(); + foreach ($polls as $poll) { + $rows[] = array( + $handles[$poll->getAuthorPHID()]->renderLink(), + phutil_render_tag( + 'a', + array( + 'href' => '/V'.$poll->getID(), + ), + phutil_escape_html('V'.$poll->getID().' '.$poll->getQuestion())), + phabricator_date($poll->getDateCreated(), $user), + phabricator_time($poll->getDateCreated(), $user), + ); + } + + $table = new AphrontTableView($rows); + $table->setColumnClasses( + array( + '', + 'pri wide', + '', + 'right', + )); + $table->setHeaders( + array( + 'Author', + 'Poll', + 'Date', + 'Time', + )); + + $headers = array( + self::VIEW_ALL + => 'Slowvotes Not Yet Consumed by the Ravages of Time', + self::VIEW_CREATED + => 'Slowvotes Birthed from Your Noblest of Great Minds', + self::VIEW_VOTED + => 'Slowvotes Within Which You Express Your Mighty Opinion', + ); + + $panel = new AphrontPanelView(); + $panel->setHeader(idx($headers, $view)); + $panel->setCreateButton('Create Slowvote', '/vote/create/'); + $panel->appendChild($table); + $panel->appendChild($pager); + + $side_nav->appendChild($panel); + + return $this->buildStandardPageResponse( + $side_nav, + array( + 'title' => 'Slowvotes', + )); + } + +} diff --git a/src/applications/slowvote/controller/list/__init__.php b/src/applications/slowvote/controller/list/__init__.php new file mode 100644 index 0000000000..2838f532c8 --- /dev/null +++ b/src/applications/slowvote/controller/list/__init__.php @@ -0,0 +1,24 @@ +id = $data['id']; + } + + public function processRequest() { + + $request = $this->getRequest(); + $user = $request->getUser(); + $viewer_phid = $user->getPHID(); + + $poll = id(new PhabricatorSlowvotePoll())->load($this->id); + if (!$poll) { + return new Aphront404Response(); + } + + $options = id(new PhabricatorSlowvoteOption())->loadAllWhere( + 'pollID = %d', + $poll->getID()); + $choices = id(new PhabricatorSlowvoteChoice())->loadAllWhere( + 'pollID = %d', + $poll->getID()); + $comments = id(new PhabricatorSlowvoteComment())->loadAllWhere( + 'pollID = %d', + $poll->getID()); + + switch ($poll->getMethod()) { + case PhabricatorSlowvotePoll::METHOD_PLURALITY: + $out_of_total = count($choices); + break; + case PhabricatorSlowvotePoll::METHOD_APPROVAL: + // Count unique respondents for approval votes. + $out_of_total = count(mpull($choices, null, 'getAuthorPHID')); + break; + default: + throw new Exception("Unknown poll method!"); + } + + $choices_by_option = mgroup($choices, 'getOptionID'); + $comments_by_user = mpull($comments, null, 'getAuthorPHID'); + $choices_by_user = mgroup($choices, 'getAuthorPHID'); + $viewer_choices = idx($choices_by_user, $viewer_phid, array()); + $viewer_comment = idx($comments_by_user, $viewer_phid, null); + + $comment_text = null; + if ($viewer_comment) { + $comment_text = $viewer_comment->getCommentText(); + } + + if ($request->isFormPost()) { + $comment = idx($comments_by_user, $viewer_phid, null); + if ($comment) { + $comment->delete(); + } + + $comment_text = $request->getStr('comments'); + if (strlen($comment_text)) { + id(new PhabricatorSlowvoteComment()) + ->setAuthorPHID($viewer_phid) + ->setPollID($poll->getID()) + ->setCommentText($comment_text) + ->save(); + } + + $votes = $request->getArr('vote'); + + switch ($poll->getMethod()) { + case PhabricatorSlowvotePoll::METHOD_PLURALITY: + // Enforce only one vote. + $votes = array_slice($votes, 0, 1); + break; + case PhabricatorSlowvotePoll::METHOD_APPROVAL: + // No filtering. + break; + default: + throw new Exception("Unknown poll method!"); + } + + foreach ($viewer_choices as $viewer_choice) { + $viewer_choice->delete(); + } + + foreach ($votes as $vote) { + id(new PhabricatorSlowvoteChoice()) + ->setAuthorPHID($viewer_phid) + ->setPollID($poll->getID()) + ->setOptionID($vote) + ->save(); + } + + return id(new AphrontRedirectResponse())->setURI('/V'.$poll->getID()); + } + + require_celerity_resource('phabricator-slowvote-css'); + + $phids = array_merge( + mpull($choices, 'getAuthorPHID'), + mpull($comments, 'getAuthorPHID'), + array( + $poll->getAuthorPHID(), + )); + + $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); + + if ($poll->getShuffle()) { + shuffle($options); + } + + $option_markup = array(); + foreach ($options as $option) { + $id = $option->getID(); + switch ($poll->getMethod()) { + case PhabricatorSlowvotePoll::METHOD_PLURALITY: + + // Render a radio button. + + $selected_option = head($viewer_choices); + if ($selected_option) { + $selected = $selected_option->getOptionID(); + } else { + $selected = null; + } + + if ($selected == $id) { + $checked = "checked"; + } else { + $checked = null; + } + + $input = phutil_render_tag( + 'input', + array( + 'type' => 'radio', + 'name' => 'vote[]', + 'value' => $id, + 'checked' => $checked, + )); + break; + case PhabricatorSlowvotePoll::METHOD_APPROVAL: + + // Render a check box. + + $checked = null; + foreach ($viewer_choices as $choice) { + if ($choice->getOptionID() == $id) { + $checked = 'checked'; + break; + } + } + + $input = phutil_render_tag( + 'input', + array( + 'type' => 'checkbox', + 'name' => 'vote[]', + 'checked' => $checked, + 'value' => $id, + )); + break; + default: + throw new Exception("Unknown poll method!"); + } + + if ($checked) { + $checked_class = 'phabricator-slowvote-checked'; + } else { + $checked_class = null; + } + + $option_markup[] = phutil_render_tag( + 'label', + array( + 'class' => 'phabricator-slowvote-label '.$checked_class, + ), + $input.phutil_escape_html($option->getName())); + } + $option_markup = implode("\n", $option_markup); + + $comments_by_option = array(); + switch ($poll->getMethod()) { + case PhabricatorSlowvotePoll::METHOD_PLURALITY: + $choice_ids = array(); + foreach ($choices_by_user as $user_phid => $choices) { + $choice_ids[$user_phid] = head($choices)->getOptionID(); + } + foreach ($comments as $comment) { + $choice = idx($choice_ids, $comment->getAuthorPHID()); + if ($choice) { + $comments_by_option[$choice][] = $comment; + } + } + break; + case PhabricatorSlowvotePoll::METHOD_APPROVAL: + // All comments are grouped in approval voting. + break; + default: + throw new Exception("Unknown poll method!"); + } + + $can_see_responses = false; + $need_vote = false; + switch ($poll->getResponseVisibility()) { + case PhabricatorSlowvotePoll::RESPONSES_VISIBLE: + $can_see_responses = true; + break; + case PhabricatorSlowvotePoll::RESPONSES_VOTERS: + $can_see_responses = (bool)$viewer_choices; + $need_vote = true; + break; + case PhabricatorSlowvotePoll::RESPONSES_OWNER: + $can_see_responses = ($viewer_phid == $poll->getAuthorPHID()); + break; + } + + $result_markup = id(new AphrontFormLayoutView()) + ->appendChild('

Ongoing Deliberation

'); + + if (!$can_see_responses) { + if ($need_vote) { + $reason = "You must vote to see the results."; + } else { + $reason = "The results are not public."; + } + $result_markup + ->appendChild( + '

'.$reason.'

'); + } else { + foreach ($options as $option) { + $id = $option->getID(); + + $chosen = idx($choices_by_option, $id, array()); + $users = array_select_keys($handles, mpull($chosen, 'getAuthorPHID')); + if ($users) { + $user_markup = array(); + foreach ($users as $handle) { + $user_markup[] = $handle->renderLink(); + } + $user_markup = implode('', $user_markup); + } else { + $user_markup = 'This option has failed to appeal to anyone.'; + } + + $comment_markup = $this->renderComments( + idx($comments_by_option, $id, array()), + $handles); + + $display = sprintf( + '%d / %d (%d%%)', + number_format(count($chosen)), + number_format($out_of_total), + $out_of_total + ? round(100 * count($chosen) / $out_of_total) + : 0); + + $result_markup->appendChild( + '
'. + '
'. + $display. + '
'. + '

'.phutil_escape_html($option->getName()).'

'. + '
'. + $user_markup. + '
'. + $comment_markup. + '
'); + } + + if ($poll->getMethod() == PhabricatorSlowvotePoll::METHOD_APPROVAL && + $comments) { + $comment_markup = $this->renderComments( + $comments, + $handles); + $result_markup->appendChild( + '

Motions Proposed for Consideration

'); + $result_markup->appendChild($comment_markup); + } + + } + + if ($viewer_choices) { + $instructions = + 'Your vote has been recorded... but there is still ample time to '. + 'rethink your position. Have you thoroughly considered all possible '. + 'eventualities?'; + } else { + $instructions = + 'This is a weighty matter indeed. Consider your choices with the '. + 'greatest of care.'; + } + + $form = id(new AphrontFormView()) + ->setUser($user) + ->appendChild( + '

'.$instructions.'

') + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel('Vote') + ->setValue($option_markup)) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel('Comments') + ->setHeight(AphrontFormTextAreaControl::HEIGHT_SHORT) + ->setName('comments') + ->setValue($comment_text)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Cautiously Engage in Deliberations')); + + + $panel = new AphrontPanelView(); + $panel->setHeader(phutil_escape_html($poll->getQuestion())); + $panel->setWidth(AphrontPanelView::WIDTH_WIDE); + + $panel->appendChild($form); + $panel->appendChild('

'); + $panel->appendChild($result_markup); + + return $this->buildStandardPageResponse( + $panel, + array( + 'title' => 'V'.$poll->getID().' '.$poll->getQuestion(), + )); + } + + private function renderComments(array $comments, array $handles) { + $viewer = $this->getRequest()->getUser(); + + $factory = new DifferentialMarkupEngineFactory(); + $engine = $factory->newDifferentialCommentMarkupEngine(); + + $comment_markup = array(); + foreach ($comments as $comment) { + $handle = $handles[$comment->getAuthorPHID()]; + + $markup = $engine->markupText($comment->getCommentText()); + + require_celerity_resource('phabricator-remarkup-css'); + + $comment_markup[] = + ''. + ''. + $handle->renderLink(). + '
'. + phabricator_datetime($comment->getDateCreated(), $viewer). + '
'. + ''. + '
'. + $markup. + '
'. + ''. + ''; + } + + if ($comment_markup) { + $comment_markup = phutil_render_tag( + 'table', + array( + 'class' => 'phabricator-slowvote-comments', + ), + implode("\n", $comment_markup)); + } else { + $comment_markup = null; + } + + return $comment_markup; + } + +} diff --git a/src/applications/slowvote/controller/poll/__init__.php b/src/applications/slowvote/controller/poll/__init__.php new file mode 100644 index 0000000000..83a100cbd9 --- /dev/null +++ b/src/applications/slowvote/controller/poll/__init__.php @@ -0,0 +1,31 @@ + true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorPHIDConstants::PHID_TYPE_POLL); + } + +} diff --git a/src/applications/slowvote/storage/poll/__init__.php b/src/applications/slowvote/storage/poll/__init__.php new file mode 100644 index 0000000000..cae6aff89b --- /dev/null +++ b/src/applications/slowvote/storage/poll/__init__.php @@ -0,0 +1,14 @@ +