From 64472dd7b8da652ff248197d4221dbff54ec3baa Mon Sep 17 00:00:00 2001 From: Pieter Hooimeijer Date: Fri, 10 Aug 2012 10:44:04 -0700 Subject: [PATCH] Adding Ponder-related files. Summary: Ponder is similar in spirit to the Wiki tool, but uses a Q&A format and up/downvotes to signal user sentiment. Popular questions are moved to the top of the feed on a 5-minute cycle based on age (younger is better) and vote count (higher is better). Pre-apologies for noob diff. Test Plan: - `./bin/phd list` Should include `PonderHeatDaemon`; phd launch it if necessary. - Navigate to /ponder/ ; observe sanity when adding questions, voting on them, and adding answers. - Confirm that questions and answers are linkable using Q5 / Q5#A5 formatted object links. - Confirm that searching for Ponder Questions works using built-in search. Feedback on code / schema / whatever organization very welcome. Reviewers: nh, vrana, epriestley Reviewed By: epriestley CC: gmarcotte, aran, Korvin, starruler Differential Revision: https://secure.phabricator.com/D3136 --- resources/sql/patches/ponder.sql | 50 +++++ src/__celerity_resource_map__.php | 183 +++++++++++++----- src/__phutil_library_map__.php | 66 +++++++ ...AphrontDefaultApplicationConfiguration.php | 1 - .../phid/PhabricatorPHIDConstants.php | 2 + .../handle/PhabricatorObjectHandleData.php | 21 ++ src/applications/ponder/PonderConstants.php | 26 +++ .../PhabricatorApplicationPonder.php | 61 ++++++ .../PonderAnswerPreviewController.php | 55 ++++++ .../controller/PonderAnswerSaveController.php | 63 ++++++ .../controller/PonderAnswerViewController.php | 41 ++++ .../ponder/controller/PonderController.php | 35 ++++ .../controller/PonderFeedController.php | 143 ++++++++++++++ .../PonderQuestionAskController.php | 152 +++++++++++++++ .../PonderQuestionPreviewController.php | 52 +++++ .../PonderQuestionViewController.php | 76 ++++++++ .../controller/PonderVoteSaveController.php | 58 ++++++ .../ponder/editor/PonderAnswerEditor.php | 64 ++++++ .../ponder/editor/PonderVoteEditor.php | 104 ++++++++++ .../ponder/query/PonderAnswerQuery.php | 161 +++++++++++++++ .../ponder/query/PonderQuestionQuery.php | 157 +++++++++++++++ .../search/PhabricatorSearchPonderIndexer.php | 56 ++++++ .../ponder/storage/PonderAnswer.php | 127 ++++++++++++ src/applications/ponder/storage/PonderDAO.php | 25 +++ .../ponder/storage/PonderQuestion.php | 141 ++++++++++++++ .../ponder/storage/PonderVotableInterface.php | 24 +++ .../ponder/view/PonderAddAnswerView.php | 97 ++++++++++ .../ponder/view/PonderAnswerListView.php | 89 +++++++++ .../ponder/view/PonderAnswerSummaryView.php | 93 +++++++++ .../ponder/view/PonderCommentBodyView.php | 149 ++++++++++++++ .../ponder/view/PonderQuestionDetailView.php | 72 +++++++ .../ponder/view/PonderQuestionFeedView.php | 102 ++++++++++ .../ponder/view/PonderQuestionSummaryView.php | 105 ++++++++++ .../ponder/view/PonderUserProfileView.php | 159 +++++++++++++++ .../ponder/view/PonderVotableView.php | 70 +++++++ .../constants/PhabricatorSearchScope.php | 2 + .../PhabricatorSearchAbstractDocument.php | 1 + .../edges/constants/PhabricatorEdgeConfig.php | 13 ++ .../markup/PhabricatorMarkupEngine.php | 7 + .../markup/rule/PonderRuleQuestion.php | 29 +++ .../patch/PhabricatorBuiltinPatchList.php | 8 + webroot/rsrc/css/application/ponder/core.css | 25 +++ webroot/rsrc/css/application/ponder/feed.css | 74 +++++++ webroot/rsrc/css/application/ponder/post.css | 34 ++++ webroot/rsrc/css/application/ponder/vote.css | 74 +++++++ webroot/rsrc/image/app/app_ponder.png | Bin 0 -> 8661 bytes .../image/application/ponder/downvote.png | Bin 0 -> 929 bytes .../rsrc/image/application/ponder/upvote.png | Bin 0 -> 916 bytes .../ponder/behavior-comment-preview.js | 32 +++ .../js/application/ponder/behavior-votebox.js | 76 ++++++++ 50 files changed, 3204 insertions(+), 51 deletions(-) create mode 100644 resources/sql/patches/ponder.sql create mode 100644 src/applications/ponder/PonderConstants.php create mode 100644 src/applications/ponder/application/PhabricatorApplicationPonder.php create mode 100644 src/applications/ponder/controller/PonderAnswerPreviewController.php create mode 100644 src/applications/ponder/controller/PonderAnswerSaveController.php create mode 100644 src/applications/ponder/controller/PonderAnswerViewController.php create mode 100644 src/applications/ponder/controller/PonderController.php create mode 100644 src/applications/ponder/controller/PonderFeedController.php create mode 100644 src/applications/ponder/controller/PonderQuestionAskController.php create mode 100644 src/applications/ponder/controller/PonderQuestionPreviewController.php create mode 100644 src/applications/ponder/controller/PonderQuestionViewController.php create mode 100644 src/applications/ponder/controller/PonderVoteSaveController.php create mode 100644 src/applications/ponder/editor/PonderAnswerEditor.php create mode 100644 src/applications/ponder/editor/PonderVoteEditor.php create mode 100644 src/applications/ponder/query/PonderAnswerQuery.php create mode 100644 src/applications/ponder/query/PonderQuestionQuery.php create mode 100644 src/applications/ponder/search/PhabricatorSearchPonderIndexer.php create mode 100644 src/applications/ponder/storage/PonderAnswer.php create mode 100644 src/applications/ponder/storage/PonderDAO.php create mode 100644 src/applications/ponder/storage/PonderQuestion.php create mode 100644 src/applications/ponder/storage/PonderVotableInterface.php create mode 100644 src/applications/ponder/view/PonderAddAnswerView.php create mode 100644 src/applications/ponder/view/PonderAnswerListView.php create mode 100644 src/applications/ponder/view/PonderAnswerSummaryView.php create mode 100644 src/applications/ponder/view/PonderCommentBodyView.php create mode 100644 src/applications/ponder/view/PonderQuestionDetailView.php create mode 100644 src/applications/ponder/view/PonderQuestionFeedView.php create mode 100644 src/applications/ponder/view/PonderQuestionSummaryView.php create mode 100644 src/applications/ponder/view/PonderUserProfileView.php create mode 100644 src/applications/ponder/view/PonderVotableView.php create mode 100644 src/infrastructure/markup/rule/PonderRuleQuestion.php create mode 100644 webroot/rsrc/css/application/ponder/core.css create mode 100644 webroot/rsrc/css/application/ponder/feed.css create mode 100644 webroot/rsrc/css/application/ponder/post.css create mode 100644 webroot/rsrc/css/application/ponder/vote.css create mode 100644 webroot/rsrc/image/app/app_ponder.png create mode 100644 webroot/rsrc/image/application/ponder/downvote.png create mode 100644 webroot/rsrc/image/application/ponder/upvote.png create mode 100644 webroot/rsrc/js/application/ponder/behavior-comment-preview.js create mode 100644 webroot/rsrc/js/application/ponder/behavior-votebox.js diff --git a/resources/sql/patches/ponder.sql b/resources/sql/patches/ponder.sql new file mode 100644 index 0000000000..1123c097a3 --- /dev/null +++ b/resources/sql/patches/ponder.sql @@ -0,0 +1,50 @@ +CREATE TABLE `{$NAMESPACE}_ponder`.`ponder_question` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `title` varchar(255) NOT NULL, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `voteCount` int(10) NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `content` longtext CHARACTER SET utf8 NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `contentSource` varchar(255) DEFAULT NULL, + `heat` float NOT NULL, + `answerCount` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `phid` (`phid`), + KEY `authorPHID` (`authorPHID`), + KEY `heat` (`heat`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `{$NAMESPACE}_ponder`.`ponder_answer` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `questionID` int(10) unsigned NOT NULL, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `voteCount` int(10) NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `content` longtext CHARACTER SET utf8 NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `contentSource` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `phid` (`phid`), + KEY `questionID` (`questionID`), + KEY `authorPHID` (`authorPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `{$NAMESPACE}_ponder`.`edge` ( + `src` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `type` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dst` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `seq` int(10) unsigned NOT NULL, + `dataID` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`src`,`type`,`dst`), + KEY `src` (`src`,`type`,`dateCreated`,`seq`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `{$NAMESPACE}_ponder`.`edgedata` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index a7e7c0a997..ae4dce01ae 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -63,6 +63,13 @@ celerity_register_resource_map(array( 'disk' => '/rsrc/image/app/app_phriction.png', 'type' => 'png', ), + '/rsrc/image/app/app_ponder.png' => + array( + 'hash' => '9893ca3674af96884d3564ecbe101280', + 'uri' => '/res/9893ca36/rsrc/image/app/app_ponder.png', + 'disk' => '/rsrc/image/app/app_ponder.png', + 'type' => 'png', + ), '/rsrc/image/app/app_settings.png' => array( 'hash' => '095fa0e61ec11d3f7e543ba48f5a5adb', @@ -70,10 +77,24 @@ celerity_register_resource_map(array( 'disk' => '/rsrc/image/app/app_settings.png', 'type' => 'png', ), + '/rsrc/image/application/ponder/downvote.png' => + array( + 'hash' => '46c5644a0fccb9e237a3363e07f50487', + 'uri' => '/res/46c5644a/rsrc/image/application/ponder/downvote.png', + 'disk' => '/rsrc/image/application/ponder/downvote.png', + 'type' => 'png', + ), + '/rsrc/image/application/ponder/upvote.png' => + array( + 'hash' => 'edd58ed3a09f3017c78601b1a5d0e7a7', + 'uri' => '/res/edd58ed3/rsrc/image/application/ponder/upvote.png', + 'disk' => '/rsrc/image/application/ponder/upvote.png', + 'type' => 'png', + ), '/rsrc/image/apps.png' => array( - 'hash' => 'f7cb4abeb73245fea4098a02fd784653', - 'uri' => '/res/f7cb4abe/rsrc/image/apps.png', + 'hash' => '134f6ac3d63e24070bf7a11b294ca72c', + 'uri' => '/res/134f6ac3/rsrc/image/apps.png', 'disk' => '/rsrc/image/apps.png', 'type' => 'png', ), @@ -1608,6 +1629,32 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/phriction/phriction-document-preview.js', ), + 'javelin-behavior-ponder-feedback-preview' => + array( + 'uri' => '/res/2e802dd9/rsrc/js/application/ponder/behavior-comment-preview.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'phabricator-shaped-request', + ), + 'disk' => '/rsrc/js/application/ponder/behavior-comment-preview.js', + ), + 'javelin-behavior-ponder-votebox' => + array( + 'uri' => '/res/6517d3f5/rsrc/js/application/ponder/behavior-votebox.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'phabricator-shaped-request', + ), + 'disk' => '/rsrc/js/application/ponder/behavior-votebox.js', + ), 'javelin-behavior-project-create' => array( 'uri' => '/res/e91f3f8f/rsrc/js/application/projects/behavior-project-create.js', @@ -1635,7 +1682,7 @@ celerity_register_resource_map(array( ), 'javelin-behavior-repository-crossreference' => array( - 'uri' => '/res/e0d58d3b/rsrc/js/application/repository/repository-crossreference.js', + 'uri' => '/res/12700384/rsrc/js/application/repository/repository-crossreference.js', 'type' => 'js', 'requires' => array( @@ -2200,7 +2247,7 @@ celerity_register_resource_map(array( ), 'phabricator-app-buttons-css' => array( - 'uri' => '/res/1e153463/rsrc/css/application/directory/phabricator-app-buttons.css', + 'uri' => '/res/2d5d414c/rsrc/css/application/directory/phabricator-app-buttons.css', 'type' => 'css', 'requires' => array( @@ -2730,6 +2777,42 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/application/phriction/phriction-document-css.css', ), + 'ponder-core-view-css' => + array( + 'uri' => '/res/4a6e2fc7/rsrc/css/application/ponder/core.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/ponder/core.css', + ), + 'ponder-feed-view-css' => + array( + 'uri' => '/res/df22bd20/rsrc/css/application/ponder/feed.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/ponder/feed.css', + ), + 'ponder-post-css' => + array( + 'uri' => '/res/32c960df/rsrc/css/application/ponder/post.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/ponder/post.css', + ), + 'ponder-vote-css' => + array( + 'uri' => '/res/923bcf97/rsrc/css/application/ponder/vote.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/ponder/vote.css', + ), 'raphael-core' => array( 'uri' => '/res/3f48575a/rsrc/js/raphael/raphael.js', @@ -2777,7 +2860,7 @@ celerity_register_resource_map(array( ), 'syntax-highlighting-css' => array( - 'uri' => '/res/5669beb6/rsrc/css/core/syntax.css', + 'uri' => '/res/cb3b9dc0/rsrc/css/core/syntax.css', 'type' => 'css', 'requires' => array( @@ -2787,7 +2870,7 @@ celerity_register_resource_map(array( ), array( 'packages' => array( - '8b65e80d' => + '04e1ab3e' => array( 'name' => 'core.pkg.css', 'symbols' => @@ -2816,7 +2899,7 @@ celerity_register_resource_map(array( 21 => 'phabricator-flag-css', 22 => 'aphront-error-view-css', ), - 'uri' => '/res/pkg/8b65e80d/core.pkg.css', + 'uri' => '/res/pkg/04e1ab3e/core.pkg.css', 'type' => 'css', ), '971b021e' => @@ -2867,7 +2950,7 @@ celerity_register_resource_map(array( 'uri' => '/res/pkg/96bc37d6/differential.pkg.css', 'type' => 'css', ), - 'ace0431a' => + 93479628 => array( 'name' => 'differential.pkg.js', 'symbols' => @@ -2891,7 +2974,7 @@ celerity_register_resource_map(array( 16 => 'javelin-behavior-differential-dropdown-menus', 17 => 'javelin-behavior-buoyant', ), - 'uri' => '/res/pkg/ace0431a/differential.pkg.js', + 'uri' => '/res/pkg/93479628/differential.pkg.js', 'type' => 'js', ), 'c8ce2d88' => @@ -2983,23 +3066,23 @@ celerity_register_resource_map(array( 'reverse' => array( 'aphront-attached-file-view-css' => '7839ae2d', - 'aphront-crumbs-view-css' => '8b65e80d', - 'aphront-dialog-view-css' => '8b65e80d', - 'aphront-error-view-css' => '8b65e80d', - 'aphront-form-view-css' => '8b65e80d', + 'aphront-crumbs-view-css' => '04e1ab3e', + 'aphront-dialog-view-css' => '04e1ab3e', + 'aphront-error-view-css' => '04e1ab3e', + 'aphront-form-view-css' => '04e1ab3e', 'aphront-headsup-action-list-view-css' => '96bc37d6', - 'aphront-headsup-view-css' => '8b65e80d', - 'aphront-list-filter-view-css' => '8b65e80d', - 'aphront-pager-view-css' => '8b65e80d', - 'aphront-panel-view-css' => '8b65e80d', - 'aphront-side-nav-view-css' => '8b65e80d', - 'aphront-table-view-css' => '8b65e80d', - 'aphront-tokenizer-control-css' => '8b65e80d', - 'aphront-tooltip-css' => '8b65e80d', - 'aphront-typeahead-control-css' => '8b65e80d', + 'aphront-headsup-view-css' => '04e1ab3e', + 'aphront-list-filter-view-css' => '04e1ab3e', + 'aphront-pager-view-css' => '04e1ab3e', + 'aphront-panel-view-css' => '04e1ab3e', + 'aphront-side-nav-view-css' => '04e1ab3e', + 'aphront-table-view-css' => '04e1ab3e', + 'aphront-tokenizer-control-css' => '04e1ab3e', + 'aphront-tooltip-css' => '04e1ab3e', + 'aphront-typeahead-control-css' => '04e1ab3e', 'differential-changeset-view-css' => '96bc37d6', 'differential-core-view-css' => '96bc37d6', - 'differential-inline-comment-editor' => 'ace0431a', + 'differential-inline-comment-editor' => '93479628', 'differential-local-commits-view-css' => '96bc37d6', 'differential-results-table-css' => '96bc37d6', 'differential-revision-add-comment-css' => '96bc37d6', @@ -3012,21 +3095,21 @@ celerity_register_resource_map(array( 'inline-comment-summary-css' => '96bc37d6', 'javelin-behavior' => '6fb20113', 'javelin-behavior-aphront-basic-tokenizer' => '97f65640', - 'javelin-behavior-aphront-drag-and-drop' => 'ace0431a', - 'javelin-behavior-aphront-drag-and-drop-textarea' => 'ace0431a', + 'javelin-behavior-aphront-drag-and-drop' => '93479628', + 'javelin-behavior-aphront-drag-and-drop-textarea' => '93479628', 'javelin-behavior-aphront-form-disable-on-submit' => '971b021e', 'javelin-behavior-audit-preview' => '5e68be89', - 'javelin-behavior-buoyant' => 'ace0431a', - 'javelin-behavior-differential-accept-with-errors' => 'ace0431a', - 'javelin-behavior-differential-add-reviewers-and-ccs' => 'ace0431a', - 'javelin-behavior-differential-comment-jump' => 'ace0431a', - 'javelin-behavior-differential-diff-radios' => 'ace0431a', - 'javelin-behavior-differential-dropdown-menus' => 'ace0431a', - 'javelin-behavior-differential-edit-inline-comments' => 'ace0431a', - 'javelin-behavior-differential-feedback-preview' => 'ace0431a', - 'javelin-behavior-differential-keyboard-navigation' => 'ace0431a', - 'javelin-behavior-differential-populate' => 'ace0431a', - 'javelin-behavior-differential-show-more' => 'ace0431a', + 'javelin-behavior-buoyant' => '93479628', + 'javelin-behavior-differential-accept-with-errors' => '93479628', + 'javelin-behavior-differential-add-reviewers-and-ccs' => '93479628', + 'javelin-behavior-differential-comment-jump' => '93479628', + 'javelin-behavior-differential-diff-radios' => '93479628', + 'javelin-behavior-differential-dropdown-menus' => '93479628', + 'javelin-behavior-differential-edit-inline-comments' => '93479628', + 'javelin-behavior-differential-feedback-preview' => '93479628', + 'javelin-behavior-differential-keyboard-navigation' => '93479628', + 'javelin-behavior-differential-populate' => '93479628', + 'javelin-behavior-differential-show-more' => '93479628', 'javelin-behavior-diffusion-commit-graph' => '5e68be89', 'javelin-behavior-diffusion-pull-lastmodified' => '5e68be89', 'javelin-behavior-maniphest-batch-selector' => '7707de41', @@ -3036,12 +3119,12 @@ celerity_register_resource_map(array( 'javelin-behavior-maniphest-transaction-preview' => '7707de41', 'javelin-behavior-phabricator-autofocus' => '971b021e', 'javelin-behavior-phabricator-keyboard-shortcuts' => '971b021e', - 'javelin-behavior-phabricator-object-selector' => 'ace0431a', + 'javelin-behavior-phabricator-object-selector' => '93479628', 'javelin-behavior-phabricator-oncopy' => '971b021e', 'javelin-behavior-phabricator-tooltips' => '971b021e', 'javelin-behavior-phabricator-watch-anchor' => '971b021e', 'javelin-behavior-refresh-csrf' => '971b021e', - 'javelin-behavior-repository-crossreference' => 'ace0431a', + 'javelin-behavior-repository-crossreference' => '93479628', 'javelin-behavior-workflow' => '971b021e', 'javelin-dom' => '6fb20113', 'javelin-event' => '6fb20113', @@ -3062,15 +3145,15 @@ celerity_register_resource_map(array( 'javelin-workflow' => '971b021e', 'maniphest-task-summary-css' => '7839ae2d', 'maniphest-transaction-detail-css' => '7839ae2d', - 'phabricator-app-buttons-css' => '8b65e80d', + 'phabricator-app-buttons-css' => '04e1ab3e', 'phabricator-content-source-view-css' => '96bc37d6', - 'phabricator-core-buttons-css' => '8b65e80d', - 'phabricator-core-css' => '8b65e80d', - 'phabricator-directory-css' => '8b65e80d', - 'phabricator-drag-and-drop-file-upload' => 'ace0431a', + 'phabricator-core-buttons-css' => '04e1ab3e', + 'phabricator-core-css' => '04e1ab3e', + 'phabricator-directory-css' => '04e1ab3e', + 'phabricator-drag-and-drop-file-upload' => '93479628', 'phabricator-dropdown-menu' => '971b021e', - 'phabricator-flag-css' => '8b65e80d', - 'phabricator-jump-nav' => '8b65e80d', + 'phabricator-flag-css' => '04e1ab3e', + 'phabricator-jump-nav' => '04e1ab3e', 'phabricator-keyboard-shortcut' => '971b021e', 'phabricator-keyboard-shortcut-manager' => '971b021e', 'phabricator-menu-item' => '971b021e', @@ -3078,11 +3161,11 @@ celerity_register_resource_map(array( 'phabricator-paste-file-upload' => '971b021e', 'phabricator-prefab' => '971b021e', 'phabricator-project-tag-css' => '7839ae2d', - 'phabricator-remarkup-css' => '8b65e80d', - 'phabricator-shaped-request' => 'ace0431a', - 'phabricator-standard-page-view' => '8b65e80d', + 'phabricator-remarkup-css' => '04e1ab3e', + 'phabricator-shaped-request' => '93479628', + 'phabricator-standard-page-view' => '04e1ab3e', 'phabricator-tooltip' => '971b021e', - 'phabricator-transaction-view-css' => '8b65e80d', - 'syntax-highlighting-css' => '8b65e80d', + 'phabricator-transaction-view-css' => '04e1ab3e', + 'syntax-highlighting-css' => '04e1ab3e', ), )); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5703f8ce9b..d7f399560d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -554,6 +554,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationManiphest' => 'applications/maniphest/application/PhabricatorApplicationManiphest.php', 'PhabricatorApplicationPeople' => 'applications/people/application/PhabricatorApplicationPeople.php', 'PhabricatorApplicationPhriction' => 'applications/phriction/application/PhabricatorApplicationPhriction.php', + 'PhabricatorApplicationPonder' => 'applications/ponder/application/PhabricatorApplicationPonder.php', 'PhabricatorApplicationProject' => 'applications/project/application/PhabricatorApplicationProject.php', 'PhabricatorApplicationSettings' => 'applications/people/application/PhabricatorApplicationSettings.php', 'PhabricatorApplicationStatusView' => 'applications/meta/view/PhabricatorApplicationStatusView.php', @@ -990,6 +991,7 @@ phutil_register_library_map(array( 'PhabricatorSearchIndexController' => 'applications/search/controller/PhabricatorSearchIndexController.php', 'PhabricatorSearchManiphestIndexer' => 'applications/search/index/indexer/PhabricatorSearchManiphestIndexer.php', 'PhabricatorSearchPhrictionIndexer' => 'applications/search/index/indexer/PhabricatorSearchPhrictionIndexer.php', + 'PhabricatorSearchPonderIndexer' => 'applications/ponder/search/PhabricatorSearchPonderIndexer.php', 'PhabricatorSearchQuery' => 'applications/search/storage/PhabricatorSearchQuery.php', 'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php', 'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php', @@ -1137,6 +1139,34 @@ phutil_register_library_map(array( 'PhrictionEditController' => 'applications/phriction/controller/PhrictionEditController.php', 'PhrictionHistoryController' => 'applications/phriction/controller/PhrictionHistoryController.php', 'PhrictionListController' => 'applications/phriction/controller/PhrictionListController.php', + 'PonderAddAnswerView' => 'applications/ponder/view/PonderAddAnswerView.php', + 'PonderAnswer' => 'applications/ponder/storage/PonderAnswer.php', + 'PonderAnswerEditor' => 'applications/ponder/editor/PonderAnswerEditor.php', + 'PonderAnswerListView' => 'applications/ponder/view/PonderAnswerListView.php', + 'PonderAnswerPreviewController' => 'applications/ponder/controller/PonderAnswerPreviewController.php', + 'PonderAnswerQuery' => 'applications/ponder/query/PonderAnswerQuery.php', + 'PonderAnswerSaveController' => 'applications/ponder/controller/PonderAnswerSaveController.php', + 'PonderAnswerSummaryView' => 'applications/ponder/view/PonderAnswerSummaryView.php', + 'PonderAnswerViewController' => 'applications/ponder/controller/PonderAnswerViewController.php', + 'PonderCommentBodyView' => 'applications/ponder/view/PonderCommentBodyView.php', + 'PonderConstants' => 'applications/ponder/PonderConstants.php', + 'PonderController' => 'applications/ponder/controller/PonderController.php', + 'PonderDAO' => 'applications/ponder/storage/PonderDAO.php', + 'PonderFeedController' => 'applications/ponder/controller/PonderFeedController.php', + 'PonderQuestion' => 'applications/ponder/storage/PonderQuestion.php', + 'PonderQuestionAskController' => 'applications/ponder/controller/PonderQuestionAskController.php', + 'PonderQuestionDetailView' => 'applications/ponder/view/PonderQuestionDetailView.php', + 'PonderQuestionFeedView' => 'applications/ponder/view/PonderQuestionFeedView.php', + 'PonderQuestionPreviewController' => 'applications/ponder/controller/PonderQuestionPreviewController.php', + 'PonderQuestionQuery' => 'applications/ponder/query/PonderQuestionQuery.php', + 'PonderQuestionSummaryView' => 'applications/ponder/view/PonderQuestionSummaryView.php', + 'PonderQuestionViewController' => 'applications/ponder/controller/PonderQuestionViewController.php', + 'PonderRuleQuestion' => 'infrastructure/markup/rule/PonderRuleQuestion.php', + 'PonderUserProfileView' => 'applications/ponder/view/PonderUserProfileView.php', + 'PonderVotableInterface' => 'applications/ponder/storage/PonderVotableInterface.php', + 'PonderVotableView' => 'applications/ponder/view/PonderVotableView.php', + 'PonderVoteEditor' => 'applications/ponder/editor/PonderVoteEditor.php', + 'PonderVoteSaveController' => 'applications/ponder/controller/PonderVoteSaveController.php', 'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php', ), 'function' => @@ -1641,6 +1671,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationManiphest' => 'PhabricatorApplication', 'PhabricatorApplicationPeople' => 'PhabricatorApplication', 'PhabricatorApplicationPhriction' => 'PhabricatorApplication', + 'PhabricatorApplicationPonder' => 'PhabricatorApplication', 'PhabricatorApplicationProject' => 'PhabricatorApplication', 'PhabricatorApplicationSettings' => 'PhabricatorApplication', 'PhabricatorApplicationStatusView' => 'AphrontView', @@ -2029,6 +2060,7 @@ phutil_register_library_map(array( 'PhabricatorSearchIndexController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchManiphestIndexer' => 'PhabricatorSearchDocumentIndexer', 'PhabricatorSearchPhrictionIndexer' => 'PhabricatorSearchDocumentIndexer', + 'PhabricatorSearchPonderIndexer' => 'PhabricatorSearchDocumentIndexer', 'PhabricatorSearchQuery' => 'PhabricatorSearchDAO', 'PhabricatorSearchResultView' => 'AphrontView', 'PhabricatorSearchSelectController' => 'PhabricatorSearchBaseController', @@ -2169,6 +2201,40 @@ phutil_register_library_map(array( 'PhrictionEditController' => 'PhrictionController', 'PhrictionHistoryController' => 'PhrictionController', 'PhrictionListController' => 'PhrictionController', + 'PonderAddAnswerView' => 'AphrontView', + 'PonderAnswer' => + array( + 0 => 'PonderDAO', + 1 => 'PhabricatorMarkupInterface', + 2 => 'PonderVotableInterface', + ), + 'PonderAnswerListView' => 'AphrontView', + 'PonderAnswerPreviewController' => 'PonderController', + 'PonderAnswerQuery' => 'PhabricatorOffsetPagedQuery', + 'PonderAnswerSaveController' => 'PonderController', + 'PonderAnswerSummaryView' => 'AphrontView', + 'PonderAnswerViewController' => 'PonderController', + 'PonderCommentBodyView' => 'AphrontView', + 'PonderController' => 'PhabricatorController', + 'PonderDAO' => 'PhabricatorLiskDAO', + 'PonderFeedController' => 'PonderController', + 'PonderQuestion' => + array( + 0 => 'PonderDAO', + 1 => 'PhabricatorMarkupInterface', + 2 => 'PonderVotableInterface', + ), + 'PonderQuestionAskController' => 'PonderController', + 'PonderQuestionDetailView' => 'AphrontView', + 'PonderQuestionFeedView' => 'AphrontView', + 'PonderQuestionPreviewController' => 'PonderController', + 'PonderQuestionQuery' => 'PhabricatorOffsetPagedQuery', + 'PonderQuestionSummaryView' => 'AphrontView', + 'PonderQuestionViewController' => 'PonderController', + 'PonderRuleQuestion' => 'PhabricatorRemarkupRuleObjectName', + 'PonderUserProfileView' => 'AphrontView', + 'PonderVotableView' => 'AphrontView', + 'PonderVoteSaveController' => 'PonderController', 'QueryFormattingTestCase' => 'PhabricatorTestCase', ), )); diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php index c946dc8883..faf49ba944 100644 --- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php @@ -320,7 +320,6 @@ class AphrontDefaultApplicationConfiguration '/emailverify/(?P[^/]+)/' => 'PhabricatorEmailVerificationController', - ); } diff --git a/src/applications/phid/PhabricatorPHIDConstants.php b/src/applications/phid/PhabricatorPHIDConstants.php index 55cde9bd0b..27318a06fb 100644 --- a/src/applications/phid/PhabricatorPHIDConstants.php +++ b/src/applications/phid/PhabricatorPHIDConstants.php @@ -42,4 +42,6 @@ final class PhabricatorPHIDConstants { const PHID_TYPE_POST = 'POST'; const PHID_TYPE_TOBJ = 'TOBJ'; const PHID_TYPE_BLOG = 'BLOG'; + const PHID_TYPE_QUES = 'QUES'; + const PHID_TYPE_ANSW = 'ANSW'; } diff --git a/src/applications/phid/handle/PhabricatorObjectHandleData.php b/src/applications/phid/handle/PhabricatorObjectHandleData.php index 9aac38a5b6..7fabb89c1f 100644 --- a/src/applications/phid/handle/PhabricatorObjectHandleData.php +++ b/src/applications/phid/handle/PhabricatorObjectHandleData.php @@ -439,6 +439,27 @@ final class PhabricatorObjectHandleData { $handles[$phid] = $handle; } break; + case PhabricatorPHIDConstants::PHID_TYPE_QUES: + $questions = id(new PonderQuestionQuery()) + ->withPHIDs($phids) + ->execute(); + $questions = mpull($questions, 'getPHID'); + + foreach ($phids as $phid) { + $handle = new PhabricatorObjectHandle(); + $handle->setPHID($phid); + $handle->setType($type); + if (empty($questions[$phid])) { + $handle->setName('Unknown Ponder Question'); + } else { + $question = $questions[$phid]; + $handle->setName(phutil_utf8_shorten($question->getTitle(), 60)); + $handle->setURI(new PhutilURI('Q' . $question->getID())); + $handle->setComplete(true); + } + $handles[$phid] = $handle; + } + break; default: $loader = null; if (isset($external_loaders[$type])) { diff --git a/src/applications/ponder/PonderConstants.php b/src/applications/ponder/PonderConstants.php new file mode 100644 index 0000000000..522cc2be7e --- /dev/null +++ b/src/applications/ponder/PonderConstants.php @@ -0,0 +1,26 @@ +\d+)' => 'PonderQuestionViewController', + '/ponder/' => array( + '(?Pfeed/)?' => 'PonderFeedController', + '(?Pprofile)/' => 'PonderFeedController', + 'answer/add/' => 'PonderAnswerSaveController', + 'answer/preview/' => 'PonderAnswerPreviewController', + 'question/ask/' => 'PonderQuestionAskController', + 'question/preview/' => 'PonderQuestionPreviewController', + '(?Pquestion)/vote/' => 'PonderVoteSaveController', + '(?Panswer)/vote/' => 'PonderVoteSaveController' + )); + } +} + diff --git a/src/applications/ponder/controller/PonderAnswerPreviewController.php b/src/applications/ponder/controller/PonderAnswerPreviewController.php new file mode 100644 index 0000000000..9eaa15797a --- /dev/null +++ b/src/applications/ponder/controller/PonderAnswerPreviewController.php @@ -0,0 +1,55 @@ +getRequest(); + $user = $request->getUser(); + $question_id = $request->getInt('question_id'); + + $question = PonderQuestionQuery::loadSingle($user, $question_id); + if (!$question) { + return new Aphront404Response(); + } + + $author_phid = $user->getPHID(); + $object_phids = array($author_phid); + $handles = id(new PhabricatorObjectHandleData($object_phids)) + ->loadHandles(); + + $answer = new PonderAnswer(); + $answer->setContent($request->getStr('content')); + $answer->setAuthorPHID($author_phid); + + $view = new PonderCommentBodyView(); + $view + ->setQuestion($question) + ->setTarget($answer) + ->setPreview(true) + ->setUser($user) + ->setHandles($handles) + ->setAction(PonderConstants::ANSWERED_LITERAL); + + return id(new AphrontAjaxResponse()) + ->setContent($view->render()); + } + +} diff --git a/src/applications/ponder/controller/PonderAnswerSaveController.php b/src/applications/ponder/controller/PonderAnswerSaveController.php new file mode 100644 index 0000000000..d350ceb0a8 --- /dev/null +++ b/src/applications/ponder/controller/PonderAnswerSaveController.php @@ -0,0 +1,63 @@ +getRequest(); + if (!$request->isFormPost()) { + return new Aphront400Response(); + } + + $user = $request->getUser(); + $question_id = $request->getInt('question_id'); + $question = PonderQuestionQuery::loadSingle($user, $question_id); + + if (!$question) { + return new Aphront404Response(); + } + + $answer = $request->getStr('answer'); + + $content_source = PhabricatorContentSource::newForSource( + PhabricatorContentSource::SOURCE_WEB, + array( + 'ip' => $request->getRemoteAddr(), + )); + + $res = new PonderAnswer(); + $res + ->setContent($answer) + ->setAuthorPHID($user->getPHID()) + ->setVoteCount(0) + ->setQuestionID($question_id) + ->setContentSource($content_source); + + id(new PonderAnswerEditor()) + ->setQuestion($question) + ->setAnswer($res) + ->saveAnswer(); + + PhabricatorSearchPonderIndexer::indexQuestion($question); + + return id(new AphrontRedirectResponse()) + ->setURI(id(new PhutilURI('/Q'. $question->getID())) + ->setFragment('A'.$res->getID())); + } + +} diff --git a/src/applications/ponder/controller/PonderAnswerViewController.php b/src/applications/ponder/controller/PonderAnswerViewController.php new file mode 100644 index 0000000000..90f3e369fa --- /dev/null +++ b/src/applications/ponder/controller/PonderAnswerViewController.php @@ -0,0 +1,41 @@ +answerID = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $answer = id(new PonderAnswer())->load($this->answerID); + + if (!$answer) { + return new Aphront404Response(); + } + + $question_id = $answer->getQuestionID(); + + return id(new AphrontRedirectResponse()) + ->setURI('/Q'.$question_id . '#A' . $answer->getID()); + } + +} diff --git a/src/applications/ponder/controller/PonderController.php b/src/applications/ponder/controller/PonderController.php new file mode 100644 index 0000000000..8e40b4a4c9 --- /dev/null +++ b/src/applications/ponder/controller/PonderController.php @@ -0,0 +1,35 @@ +buildStandardPageView(); + $page->setApplicationName('Ponder!'); + $page->setBaseURI('/ponder/'); + $page->setTitle(idx($data, 'title')); + $page->setGlyph("\xE2\x97\xB3"); + $page->appendChild($view); + $page->setSearchDefaultScope(PhabricatorSearchScope::SCOPE_QUESTIONS); + + $response = new AphrontWebpageResponse(); + return $response->setContent($page->render()); + } + +} diff --git a/src/applications/ponder/controller/PonderFeedController.php b/src/applications/ponder/controller/PonderFeedController.php new file mode 100644 index 0000000000..645f248ba0 --- /dev/null +++ b/src/applications/ponder/controller/PonderFeedController.php @@ -0,0 +1,143 @@ + "Popular Questions", + self::PAGE_PROFILE => "User Profile" + ); + + public function willProcessRequest(array $data) { + if (isset($data['page'])) { + $this->page = $data['page']; + } + + if (!isset(self::$pages[$this->page])) { + $this->page = self::PAGE_FEED; + } + + $this->feedOffset = 0; + if (isset($data['feedoffset'])) { + $this->feedOffset = $data['feedoffset']; + } + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + $this->feedOffset = $request->getInt('off'); + $this->questionOffset = $request->getInt('qoff'); + $this->answerOffset = $request->getInt('aoff'); + + $side_nav = new AphrontSideNavView(); + foreach (self::$pages as $pagename => $pagetitle) { + $class = ""; + if ($pagename == $this->page) { + $class = 'aphront-side-nav-selected'; + } + + $linky = phutil_render_tag( + 'a', + array( + 'href' => '/ponder/'.$pagename .'/', + 'class' => $class + ), + phutil_escape_html($pagetitle) + ); + + $side_nav->addNavItem($linky); + } + + switch ($this->page) { + case self::PAGE_FEED: + $data = PonderQuestionQuery::loadHottest( + $user, + $this->feedOffset, + self::FEED_PAGE_SIZE + 1); + + $phids = array(); + foreach ($data as $question) { + $phids[] = $question->getAuthorPHID(); + } + $handles = id(new PhabricatorObjectHandleData($phids)) + ->loadHandles(); + + $side_nav->appendChild( + id(new PonderQuestionFeedView()) + ->setUser($user) + ->setData($data) + ->setHandles($handles) + ->setOffset($this->feedOffset) + ->setPageSize(self::FEED_PAGE_SIZE) + ->setURI(new PhutilURI("/ponder/feed/"), "off") + ); + break; + case self::PAGE_PROFILE: + $questions = PonderQuestionQuery::loadByAuthor( + $user, + $user->getPHID(), + $this->questionOffset, + self::PROFILE_QUESTION_PAGE_SIZE + 1 + ); + + $answers = PonderAnswerQuery::loadByAuthorWithQuestions( + $user, + $user->getPHID(), + $this->answerOffset, + self::PROFILE_ANSWER_PAGE_SIZE + 1 + ); + + $phids = array($user->getPHID()); + $handles = id(new PhabricatorObjectHandleData($phids)) + ->loadHandles(); + + $side_nav->appendChild( + id(new PonderUserProfileView()) + ->setUser($user) + ->setQuestions($questions) + ->setAnswers($answers) + ->setHandles($handles) + ->setQuestionOffset($this->questionOffset) + ->setAnswerOffset($this->answerOffset) + ->setPageSize(self::PROFILE_QUESTION_PAGE_SIZE) + ->setURI(new PhutilURI("/ponder/profile/"), "qoff", "aoff") + ); + break; + } + + + return $this->buildStandardPageResponse( + $side_nav, + array( + 'title' => self::$pages[$this->page] + )); + } + +} diff --git a/src/applications/ponder/controller/PonderQuestionAskController.php b/src/applications/ponder/controller/PonderQuestionAskController.php new file mode 100644 index 0000000000..8b622fc4cd --- /dev/null +++ b/src/applications/ponder/controller/PonderQuestionAskController.php @@ -0,0 +1,152 @@ +getRequest(); + $user = $request->getUser(); + + if ($request->isFormPost()) { + return $this->handlePost(); + } + + return $this->showForm(); + } + + private function handlePost() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $errors = array(); + $title = $request->getStr('title'); + $content = $request->getStr('content'); + + // form validation + if (phutil_utf8_strlen($title) < 1 || phutil_utf8_strlen($title) > 255) { + $errors[] = "Please enter a title (1-255 characters)"; + } + + if ($errors) { + return $this->showForm($errors, $title, $content); + } + + // no validation errors -> save it + + $content_source = PhabricatorContentSource::newForSource( + PhabricatorContentSource::SOURCE_WEB, + array( + 'ip' => $request->getRemoteAddr(), + )); + + $question = id(new PonderQuestion()) + ->setTitle($title) + ->setContent($content) + ->setAuthorPHID($user->getPHID()) + ->setContentSource($content_source) + ->setVoteCount(0) + ->setAnswerCount(0) + ->setHeat(0.0) + ->save(); + + PhabricatorSearchPonderIndexer::indexQuestion($question); + + return id(new AphrontRedirectResponse()) + ->setURI('/Q'.$question->getID()); + } + + private function showForm( + $errors = null, + $title = "", + $content = "", + $id = null) { + + require_celerity_resource('ponder-core-view-css'); + require_celerity_resource('phabricator-remarkup-css'); + require_celerity_resource('ponder-post-css'); + + $request = $this->getRequest(); + $user = $request->getUser(); + $error_view = null; + + if ($errors) { + $error_view = id(new AphrontErrorView()) + ->setTitle('Form Errors') + ->setErrors($errors); + } + + $form = new AphrontFormView(); + $form->setUser($user); + $form->setAction('/ponder/question/ask/'); + $form + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Title') + ->setName('title') + ->setValue($title)) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setName('content') + ->setID('content') + ->setValue($content) + ->setLabel("Question") + ->setCaption(phutil_render_tag( + 'a', + array( + 'href' => PhabricatorEnv::getDoclink( + 'article/Remarkup_Reference.html'), + 'tabindex' => '-1', + 'target' => '_blank', + ), + "Formatting Reference"))) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Ask Away!')); + + $panel = id(new AphrontPanelView()) + ->addClass("ponder-panel") + ->setHeader("Your Question:") + ->appendChild($error_view) + ->appendChild($form); + + $panel->appendChild( + '
'. + '
'. + ''. + 'Loading question preview...'. + ''. + '
'. + '
' + ); + + Javelin::initBehavior( + 'ponder-feedback-preview', + array( + 'uri' => '/ponder/question/preview/', + 'content' => 'content', + 'preview' => 'question-preview', + 'question_id' => null + )); + + return $this->buildStandardPageResponse( + array($panel), + array('title' => 'Ask a Question') + ); + } + +} diff --git a/src/applications/ponder/controller/PonderQuestionPreviewController.php b/src/applications/ponder/controller/PonderQuestionPreviewController.php new file mode 100644 index 0000000000..1dff4a58a5 --- /dev/null +++ b/src/applications/ponder/controller/PonderQuestionPreviewController.php @@ -0,0 +1,52 @@ +getRequest(); + + $user = $request->getUser(); + $author_phid = $user->getPHID(); + + $object_phids = array($author_phid); + $handles = id(new PhabricatorObjectHandleData($object_phids)) + ->loadHandles(); + + $question = new PonderQuestion(); + $question->setContent($request->getStr('content')); + $question->setAuthorPHID($author_phid); + + $view = new PonderCommentBodyView(); + $view + ->setQuestion($question) + ->setTarget($question) + ->setPreview(true) + ->setUser($user) + ->setHandles($handles) + ->setAction(self::VERB_ASKED); + + return id(new AphrontAjaxResponse()) + ->setContent($view->render()); + } + +} diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php new file mode 100644 index 0000000000..c3356a2ee6 --- /dev/null +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -0,0 +1,76 @@ +questionID = $data['id']; + } + + public function processRequest() { + + $request = $this->getRequest(); + $user = $request->getUser(); + + $question = PonderQuestionQuery::loadSingle($user, $this->questionID); + if (!$question) { + return new Aphront404Response(); + } + $question->attachRelated($user->getPHID()); + $answers = $question->getAnswers(); + + $object_phids = array($user->getPHID(), $question->getAuthorPHID()); + foreach ($answers as $answer) { + $object_phids[] = $answer->getAuthorPHID(); + } + + $handles = id(new PhabricatorObjectHandleData($object_phids)) + ->loadHandles(); + + $detail_panel = new PonderQuestionDetailView(); + $detail_panel + ->setQuestion($question) + ->setUser($user) + ->setHandles($handles); + + $responses_panel = new PonderAnswerListView(); + $responses_panel + ->setQuestion($question) + ->setHandles($handles) + ->setUser($user) + ->setAnswers($answers); + + $answer_add_panel = new PonderAddAnswerView(); + $answer_add_panel + ->setQuestion($question) + ->setUser($user) + ->setActionURI("/ponder/answer/add/"); + + return $this->buildStandardPageResponse( + array( + $detail_panel, + $responses_panel, + $answer_add_panel + ), + array( + 'title' => 'Q'.$question->getID().' '.$question->getTitle() + )); + } +} diff --git a/src/applications/ponder/controller/PonderVoteSaveController.php b/src/applications/ponder/controller/PonderVoteSaveController.php new file mode 100644 index 0000000000..6d58ef946d --- /dev/null +++ b/src/applications/ponder/controller/PonderVoteSaveController.php @@ -0,0 +1,58 @@ +kind = $data['kind']; + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + $newvote = $request->getInt("vote"); + $phid = $request->getStr("phid"); + + if (1 < $newvote || $newvote < -1) { + return new Aphront400Response(); + } + + $target = null; + + if ($this->kind == "question") { + $target = PonderQuestionQuery::loadSingleByPHID($user, $phid); + } + else if ($this->kind == "answer") { + $target = PonderAnswerQuery::loadSingleByPHID($user, $phid); + } + + if (!$target) { + return new Aphront404Response(); + } + + $editor = id(new PonderVoteEditor()) + ->setVotable($target) + ->setUser($user) + ->setVote($newvote) + ->saveVote(); + + return id(new AphrontAjaxResponse())->setContent("."); + } +} diff --git a/src/applications/ponder/editor/PonderAnswerEditor.php b/src/applications/ponder/editor/PonderAnswerEditor.php new file mode 100644 index 0000000000..7cad5d7391 --- /dev/null +++ b/src/applications/ponder/editor/PonderAnswerEditor.php @@ -0,0 +1,64 @@ +question = $question; + return $this; + } + + public function setAnswer($answer) { + $this->answer = $answer; + return $this; + } + + public function saveAnswer() { + if (!$this->question) { + throw new Exception("Must set question before saving vote"); + } + if (!$this->answer) { + throw new Exception("Must set answer before saving vote"); + } + + $question = $this->question; + $answer = $this->answer; + $conn = $answer->establishConnection('w'); + $trans = $conn->openTransaction(); + $trans->beginReadLocking(); + + $question->reload(); + + queryfx($conn, + 'UPDATE %T as t + SET t.`answerCount` = t.`answerCount` + 1 + WHERE t.`PHID` = %s', + $question->getTableName(), + $question->getPHID()); + + $answer->setQuestionID($question->getID()); + $answer->save(); + + $trans->endReadLocking(); + $trans->saveTransaction(); + } +} diff --git a/src/applications/ponder/editor/PonderVoteEditor.php b/src/applications/ponder/editor/PonderVoteEditor.php new file mode 100644 index 0000000000..4a6090d497 --- /dev/null +++ b/src/applications/ponder/editor/PonderVoteEditor.php @@ -0,0 +1,104 @@ +answer = $answer; + return $this; + } + + public function setVotable($votable) { + $this->votable = $votable; + return $this; + } + + public function setUser($user) { + $this->user = $user; + return $this; + } + + public function setVote($vote) { + $this->vote = $vote; + return $this; + } + + public function saveVote() { + if (!$this->votable) { + throw new Exception("Must set votable before saving vote"); + } + if (!$this->user) { + throw new Exception("Must set user before saving vote"); + } + + $user = $this->user; + $votable = $this->votable; + $newvote = $this->vote; + + // prepare vote add, or update if this user is amending an + // earlier vote + $editor = id(new PhabricatorEdgeEditor()) + ->setUser($user) + ->addEdge( + $user->getPHID(), + $votable->getUserVoteEdgeType(), + $votable->getVotablePHID(), + array('data' => $newvote)) + ->removeEdge( + $user->getPHID(), + $votable->getUserVoteEdgeType(), + $votable->getVotablePHID()); + + $conn = $votable->establishConnection('w'); + $trans = $conn->openTransaction(); + $trans->beginReadLocking(); + + $votable->reload(); + $curvote = (int)PhabricatorEdgeQuery::loadSingleEdgeData( + $user->getPHID(), + $votable->getUserVoteEdgeType(), + $votable->getVotablePHID()); + + if (!$curvote) { + $curvote = PonderConstants::NONE_VOTE; + } + + // adjust votable's score by this much + $delta = $newvote - $curvote; + + queryfx($conn, + 'UPDATE %T as t + SET t.`voteCount` = t.`voteCount` + %d + WHERE t.`PHID` = %s', + $votable->getTableName(), + $delta, + $votable->getVotablePHID()); + + $editor->save(); + + $trans->endReadLocking(); + $trans->saveTransaction(); + } +} diff --git a/src/applications/ponder/query/PonderAnswerQuery.php b/src/applications/ponder/query/PonderAnswerQuery.php new file mode 100644 index 0000000000..4e8c75bcf2 --- /dev/null +++ b/src/applications/ponder/query/PonderAnswerQuery.php @@ -0,0 +1,161 @@ +id = $qid; + return $this; + } + + public function withPHID($phid) { + $this->phid = $phid; + return $this; + } + + public function withAuthorPHID($phid) { + $this->authorPHID = $phid; + return $this; + } + + public function orderByNewest($usethis) { + $this->orderNewest = $usethis; + return $this; + } + + public static function loadByAuthorWithQuestions( + $viewer, + $phid, + $offset, + $count) { + + if (!$viewer) { + throw new Exception("Must set viewer when calling loadByAuthor..."); + } + + $answers = id(new PonderAnswerQuery()) + ->withAuthorPHID($phid) + ->orderByNewest(true) + ->setOffset($offset) + ->setLimit($count) + ->execute(); + + $answerset = new LiskDAOSet(); + foreach ($answers as $answer) { + $answerset->addToSet($answer); + } + + foreach ($answers as $answer) { + $question = $answer->loadOneRelative( + new PonderQuestion(), + 'id', + 'getQuestionID'); + $answer->setQuestion($question); + } + + return $answers; + } + + public static function loadByAuthor($viewer, $author_phid, $offset, $count) { + if (!$viewer) { + throw new Exception("Must set viewer when calling loadByAuthor"); + } + + return id(new PonderAnswerQuery()) + ->withAuthorPHID($author_phid) + ->setOffset($offset) + ->setLimit($count) + ->orderByNewest(true) + ->execute(); + } + + public static function loadSingle($viewer, $id) { + if (!$viewer) { + throw new Exception("Must set viewer when calling loadSingle"); + } + return idx(id(new PonderAnswerQuery()) + ->withID($id) + ->execute(), $id); + } + + public static function loadSingleByPHID($viewer, $phid) { + if (!$viewer) { + throw new Exception("Must set viewer when calling loadSingle"); + } + + return array_shift(id(new PonderAnswerQuery()) + ->withPHID($phid) + ->execute()); + } + + private function buildWhereClause($conn_r) { + $where = array(); + if ($this->id) { + $where[] = qsprintf($conn_r, '(id = %d)', $this->id); + } + if ($this->phid) { + $where[] = qsprintf($conn_r, '(phid = %s)', $this->phid); + } + if ($this->authorPHID) { + $where[] = qsprintf($conn_r, '(authorPHID = %s)', $this->authorPHID); + } + + return $this->formatWhereClause($where); + } + + private function buildOrderByClause($conn_r) { + $order = array(); + if ($this->orderNewest) { + $order[] = qsprintf($conn_r, 'id DESC'); + } + + if (count($order) == 0) { + $order[] = qsprintf($conn_r, 'id ASC'); + } + + return ($order ? 'ORDER BY ' . implode(', ', $order) : ''); + } + + public function execute() { + $answer = new PonderAnswer(); + $conn_r = $answer->establishConnection('r'); + + $select = qsprintf( + $conn_r, + 'SELECT r.* FROM %T r', + $answer->getTableName()); + + $where = $this->buildWhereClause($conn_r); + $order_by = $this->buildOrderByClause($conn_r); + $limit = $this->buildLimitClause($conn_r); + + return $answer->loadAllFromArray( + queryfx_all( + $conn_r, + '%Q %Q %Q %Q', + $select, + $where, + $order_by, + $limit)); + } +} diff --git a/src/applications/ponder/query/PonderQuestionQuery.php b/src/applications/ponder/query/PonderQuestionQuery.php new file mode 100644 index 0000000000..0b587f0c3d --- /dev/null +++ b/src/applications/ponder/query/PonderQuestionQuery.php @@ -0,0 +1,157 @@ +id = $qid; + return $this; + } + + public function withPHID($phid) { + $this->phids = array($phid); + return $this; + } + + public function withPHIDs($phids) { + $this->phids = $phids; + return $this; + } + + public function withAuthorPHID($phid) { + $this->authorPHID = $phid; + return $this; + } + + public function orderByHottest($usethis) { + $this->orderHottest = $usethis; + return $this; + } + + public function orderByNewest($usethis) { + $this->orderNewest = $usethis; + return $this; + } + + public static function loadHottest($viewer, $offset, $count) { + if (!$viewer) { + throw new Exception("Must set viewer when calling loadHottest"); + } + + return id(new PonderQuestionQuery()) + ->setOffset($offset) + ->setLimit($count) + ->orderByHottest(true) + ->execute(); + } + + public static function loadByAuthor($viewer, $author_phid, $offset, $count) { + if (!$viewer) { + throw new Exception("Must set viewer when calling loadByAuthor"); + } + + return id(new PonderQuestionQuery()) + ->withAuthorPHID($author_phid) + ->setOffset($offset) + ->setLimit($count) + ->orderByNewest(true) + ->execute(); + } + + public static function loadSingle($viewer, $id) { + if (!$viewer) { + throw new Exception("Must set viewer when calling loadSingle"); + } + + return idx(id(new PonderQuestionQuery()) + ->withID($id) + ->execute(), $id); + } + + public static function loadSingleByPHID($viewer, $phid) { + if (!$viewer) { + throw new Exception("Must set viewer when calling loadSingle"); + } + + return array_shift(id(new PonderQuestionQuery()) + ->withPHID($phid) + ->execute()); + } + + private function buildWhereClause($conn_r) { + $where = array(); + if ($this->id) { + $where[] = qsprintf($conn_r, '(id = %d)', $this->id); + } + if ($this->phids) { + $where[] = qsprintf($conn_r, '(phid in (%Ls))', $this->phids); + } + if ($this->authorPHID) { + $where[] = qsprintf($conn_r, '(authorPHID = %s)', $this->authorPHID); + } + + return ($where ? 'WHERE ' . implode(' AND ', $where) : ''); + } + + private function buildOrderByClause($conn_r) { + $order = array(); + if ($this->orderHottest) { + $order[] = qsprintf($conn_r, 'heat DESC'); + } + if ($this->orderNewest) { + $order[] = qsprintf($conn_r, 'id DESC'); + } + + if (count($order) == 0) { + $order[] = qsprintf($conn_r, 'id ASC'); + } + + return ($order ? 'ORDER BY ' . implode(', ', $order) : ''); + } + + public function execute() { + $question = new PonderQuestion(); + $conn_r = $question->establishConnection('r'); + + $select = qsprintf( + $conn_r, + 'SELECT r.* FROM %T r', + $question->getTableName()); + + $where = $this->buildWhereClause($conn_r); + $order_by = $this->buildOrderByClause($conn_r); + $limit = $this->buildLimitClause($conn_r); + + return $question->loadAllFromArray( + queryfx_all( + $conn_r, + '%Q %Q %Q %Q', + $select, + $where, + $order_by, + $limit)); + } + + +} diff --git a/src/applications/ponder/search/PhabricatorSearchPonderIndexer.php b/src/applications/ponder/search/PhabricatorSearchPonderIndexer.php new file mode 100644 index 0000000000..b8ff08c5f2 --- /dev/null +++ b/src/applications/ponder/search/PhabricatorSearchPonderIndexer.php @@ -0,0 +1,56 @@ +setPHID($question->getPHID()); + $doc->setDocumentType(PhabricatorPHIDConstants::PHID_TYPE_QUES); + $doc->setDocumentTitle($question->getTitle()); + $doc->setDocumentCreated($question->getDateCreated()); + $doc->setDocumentModified($question->getDateModified()); + + $doc->addField( + PhabricatorSearchField::FIELD_BODY, + $question->getContent()); + + $doc->addRelationship( + PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR, + $question->getAuthorPHID(), + PhabricatorPHIDConstants::PHID_TYPE_USER, + $question->getDateCreated()); + + $answers = $question->getAnswers(); + $touches = array(); + foreach ($answers as $comment) { + if (strlen($comment->getContent())) { + $doc->addField( + PhabricatorSearchField::FIELD_COMMENT, + $comment->getContent()); + } + + $author = $comment->getAuthorPHID(); + $touches[$author] = $comment->getDateCreated(); + } + + self::reindexAbstractDocument($doc); + } +} diff --git a/src/applications/ponder/storage/PonderAnswer.php b/src/applications/ponder/storage/PonderAnswer.php new file mode 100644 index 0000000000..26fc26ac02 --- /dev/null +++ b/src/applications/ponder/storage/PonderAnswer.php @@ -0,0 +1,127 @@ +question = $question; + return $this; + } + + public function getQuestion() { + return $this->question; + } + + public function setUserVote($vote) { + $this->vote = $vote['data']; + if (!$this->vote) { + $this->vote = PonderConstants::NONE_VOTE; + } + return $this; + } + + public function getUserVote() { + return $this->vote; + } + + public function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + ) + parent::getConfiguration(); + } + + public function setTitle($title) { + $this->title = $title; + if (!$this->getID()) { + $this->originalTitle = $title; + } + return $this; + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorPHIDConstants::PHID_TYPE_ANSW); + } + + public function setContentSource(PhabricatorContentSource $content_source) { + $this->contentSource = $content_source->serialize(); + return $this; + } + + public function getContentSource() { + return PhabricatorContentSource::newFromSerialized($this->contentSource); + } + + public function getAnswers() { + return $this->loadRelatives(new PonderAnswer(), "questionID"); + } + + public function getMarkupField() { + return self::MARKUP_FIELD_CONTENT; + } + + // Markup interface + + public function getMarkupFieldKey($field) { + $hash = PhabricatorHash::digest($this->getMarkupText($field)); + $id = $this->getID(); + return "ponder:A{$id}:{$field}:{$hash}"; + } + + public function getMarkupText($field) { + return $this->getContent(); + } + + public function newMarkupEngine($field) { + return PhabricatorMarkupEngine::newPonderMarkupEngine(); + } + + public function didMarkupText( + $field, + $output, + PhutilMarkupEngine $engine) { + return $output; + } + + public function shouldUseMarkupCache($field) { + return (bool)$this->getID(); + } + + // votable interface + public function getUserVoteEdgeType() { + return PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_ANSWER; + } + + public function getVotablePHID() { + return $this->getPHID(); + } +} diff --git a/src/applications/ponder/storage/PonderDAO.php b/src/applications/ponder/storage/PonderDAO.php new file mode 100644 index 0000000000..f42e1966e2 --- /dev/null +++ b/src/applications/ponder/storage/PonderDAO.php @@ -0,0 +1,25 @@ + true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorPHIDConstants::PHID_TYPE_QUES); + } + + public function setContentSource(PhabricatorContentSource $content_source) { + $this->contentSource = $content_source->serialize(); + return $this; + } + + public function getContentSource() { + return PhabricatorContentSource::newFromSerialized($this->contentSource); + } + + public function attachRelated($user_phid) { + $this->answers = $this->loadRelatives(new PonderAnswer(), "questionID"); + $qa_phids = mpull($this->answers, 'getPHID') + array($this->getPHID()); + + $edges = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(array($user_phid)) + ->withDestinationPHIDs($qa_phids) + ->withEdgeTypes( + array( + PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_QUESTION, + PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_ANSWER + )) + ->needEdgeData(true) + ->execute(); + + $question_edge = + $edges[$user_phid][PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_QUESTION]; + $answer_edges = + $edges[$user_phid][PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_ANSWER]; + $edges = null; + + $this->setUserVote(idx($question_edge, $this->getPHID())); + foreach ($this->answers as $answer) { + $answer->setQuestion($this); + $answer->setUserVote(idx($answer_edges, $answer->getPHID())); + } + } + + public function setUserVote($vote) { + $this->vote = $vote['data']; + if (!$this->vote) { + $this->vote = PonderConstants::NONE_VOTE; + } + return $this; + } + + public function getUserVote() { + return $this->vote; + } + + public function getAnswers() { + return $this->answers; + } + + public function getMarkupField() { + return self::MARKUP_FIELD_CONTENT; + } + + // 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::newPonderMarkupEngine(); + } + + public function didMarkupText( + $field, + $output, + PhutilMarkupEngine $engine) { + return $output; + } + + public function shouldUseMarkupCache($field) { + return (bool)$this->getID(); + } + + // votable interface + public function getUserVoteEdgeType() { + return PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_QUESTION; + } + + public function getVotablePHID() { + return $this->getPHID(); + } +} diff --git a/src/applications/ponder/storage/PonderVotableInterface.php b/src/applications/ponder/storage/PonderVotableInterface.php new file mode 100644 index 0000000000..14c277c2d7 --- /dev/null +++ b/src/applications/ponder/storage/PonderVotableInterface.php @@ -0,0 +1,24 @@ +question = $question; + return $this; + } + + public function setUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + + public function setActionURI($uri) { + $this->actionURI = $uri; + return $this; + } + + public function render() { + require_celerity_resource('ponder-core-view-css'); + + $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); + + $question = $this->question; + + $panel = id(new AphrontPanelView()) + ->addClass("ponder-panel") + ->setHeader("Your Answer:"); + + $form = new AphrontFormView(); + $form + ->setUser($this->user) + ->setAction($this->actionURI) + ->addHiddenInput('question_id', $question->getID()) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setName('answer') + ->setID('answer-content') + ->setEnableDragAndDropFileUploads(true) + ->setCaption(phutil_render_tag( + 'a', + array( + 'href' => PhabricatorEnv::getDoclink( + 'article/Remarkup_Reference.html'), + 'tabindex' => '-1', + 'target' => '_blank', + ), + 'Formatting Reference'))) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue($is_serious ? 'Submit' : 'Make it so.')); + + $panel->appendChild($form); + $panel->appendChild( + '
'. + '
'. + ''. + 'Loading answer preview...'. + ''. + '
'. + '
' + ); + + Javelin::initBehavior( + 'ponder-feedback-preview', + array( + 'uri' => '/ponder/answer/preview/', + 'content' => 'answer-content', + 'preview' => 'answer-preview', + 'question_id' => $question->getID() + )); + + return $panel->render(); + } +} diff --git a/src/applications/ponder/view/PonderAnswerListView.php b/src/applications/ponder/view/PonderAnswerListView.php new file mode 100644 index 0000000000..dd4034be23 --- /dev/null +++ b/src/applications/ponder/view/PonderAnswerListView.php @@ -0,0 +1,89 @@ +question = $question; + return $this; + } + + public function setHandles(array $handles) { + assert_instances_of($handles, 'PhabricatorObjectHandle'); + $this->handles = $handles; + return $this; + } + + public function setUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + + public function setAnswers(array $answers) { + assert_instances_of($answers, 'PonderAnswer'); + + $this->answers = array(); + + // group by descreasing score, randomizing + // order within groups + $by_score = mgroup($answers, 'getVoteCount'); + $scores = array_keys($by_score); + rsort($scores); + + foreach ($scores as $score) { + $group = $by_score[$score]; + shuffle($group); + foreach ($group as $cur_answer) { + $this->answers[] = $cur_answer; + } + } + + return $this; + } + + public function render() { + require_celerity_resource('ponder-post-css'); + + $question = $this->question; + $user = $this->user; + $handles = $this->handles; + + $panel = id(new AphrontPanelView()) + ->addClass("ponder-panel") + ->setHeader("Responses:"); + + foreach ($this->answers as $cur_answer) { + $view = new PonderCommentBodyView(); + $view + ->setQuestion($question) + ->setTarget($cur_answer) + ->setAction(PonderConstants::ANSWERED_LITERAL) + ->setHandles($handles) + ->setUser($user); + + $panel->appendChild($view); + } + + return $panel->render(); + } +} diff --git a/src/applications/ponder/view/PonderAnswerSummaryView.php b/src/applications/ponder/view/PonderAnswerSummaryView.php new file mode 100644 index 0000000000..5e7989e449 --- /dev/null +++ b/src/applications/ponder/view/PonderAnswerSummaryView.php @@ -0,0 +1,93 @@ +answer = $answer; + return $this; + } + + public function setHandles($handles) { + $this->handles = $handles; + return $this; + } + + public function setUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + + private static function abbreviate($w) { + return phutil_utf8_shorten($w, 60); + } + + public function render() { + require_celerity_resource('ponder-feed-view-css'); + + $user = $this->user; + $answer = $this->answer; + $question = $answer->getQuestion(); + $author_phid = $question->getAuthorPHID(); + $handles = $this->handles; + + $votecount = + '
'. + phutil_escape_html($answer->getVoteCount()). + '
'. + 'votes'. + '
'. + '
'; + + $title = + '

'. + phutil_render_tag( + 'a', + array( + "href" => id(new PhutilURI('/Q' . $question->getID())) + ->setFragment('A' . $answer->getID()) + ), + phutil_escape_html('A' . $answer->getID() . ' ' . + self::abbreviate($answer->getContent()) + ) + ). + '

'; + + $rhs = + ''; + + $summary = + '
'. + $votecount. + $rhs. + '
'; + + return $summary; + } +} diff --git a/src/applications/ponder/view/PonderCommentBodyView.php b/src/applications/ponder/view/PonderCommentBodyView.php new file mode 100644 index 0000000000..4bf0a57ad3 --- /dev/null +++ b/src/applications/ponder/view/PonderCommentBodyView.php @@ -0,0 +1,149 @@ +question = $question; + return $this; + } + + public function setTarget($target) { + $this->target = $target; + return $this; + } + + public function setAction($action) { + $this->action = $action; + return $this; + } + + public function setHandles(array $handles) { + assert_instances_of($handles, 'PhabricatorObjectHandle'); + $this->handles = $handles; + return $this; + } + + public function setPreview($preview) { + $this->preview = $preview; + return $this; + } + + public function setUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + + public function render() { + + if (!$this->user) { + throw new Exception("Call setUser() before rendering!"); + } + + require_celerity_resource('phabricator-remarkup-css'); + require_celerity_resource('ponder-post-css'); + + $user = $this->user; + $question = $this->question; + $target = $this->target; + $content = $target->getContent(); + $info = array(); + + + $content = PhabricatorMarkupEngine::renderOneObject( + $target, + $target->getMarkupField()); + + $content = + '
'. + $content. + '
'; + + $author = $this->handles[$target->getAuthorPHID()]; + $actions = array($author->renderLink().' '.$this->action); + $author_link = $author->renderLink(); + $xaction_view = id(new PhabricatorTransactionView()) + ->setUser($user) + ->setImageURI($author->getImageURI()) + ->setContentSource($target->getContentSource()) + ->setActions($actions); + + if ($this->target instanceof PonderAnswer) { + $xaction_view->addClass("ponder-answer"); + } + else { + $xaction_view->addClass("ponder-question"); + } + + if ($this->preview) { + $xaction_view->setIsPreview($this->preview); + } else { + $xaction_view->setEpoch($target->getDateCreated()); + if ($this->target instanceof PonderAnswer) { + $anchor_text = 'Q' . $question->getID(). '#A' . $target->getID(); + $xaction_view->setAnchor('A'.$target->getID(), $anchor_text); + $xaction_view->addClass("ponder-answer"); + } + } + + $xaction_view->appendChild( + '
'. + $content. + '
' + ); + + $outerview = $xaction_view; + if (!$this->preview) { + $outerview = + id(new PonderVotableView()) + ->setPHID($target->getPHID()) + ->setCount($target->getVoteCount()) + ->setVote($target->getUserVote()); + + if ($this->target instanceof PonderAnswer) { + $outerview->setURI('/ponder/answer/vote/'); + } + else { + $outerview->setURI('/ponder/question/vote/'); + } + + $outerview->appendChild($xaction_view); + } + + return $outerview->render(); + } + + private function renderHandleList(array $phids) { + $result = array(); + foreach ($phids as $phid) { + $result[] = $this->handles[$phid]->renderLink(); + } + return implode(', ', $result); + } + + + +} diff --git a/src/applications/ponder/view/PonderQuestionDetailView.php b/src/applications/ponder/view/PonderQuestionDetailView.php new file mode 100644 index 0000000000..7e9f56dcc5 --- /dev/null +++ b/src/applications/ponder/view/PonderQuestionDetailView.php @@ -0,0 +1,72 @@ +question = $question; + return $this; + } + + public function setUser($user) { + $this->user = $user; + return $this; + } + + public function setHandles($handles) { + $this->handles = $handles; + return $this; + } + + public function render() { + require_celerity_resource('ponder-core-view-css'); + + $question = $this->question; + $handles = $this->handles; + $user = $this->user; + + $panel = id(new AphrontPanelView()) + ->addClass("ponder-panel") + ->setHeader($this->renderObjectLink().' '.$question->getTitle()); + + $contentview = new PonderCommentBodyView(); + $contentview + ->setTarget($question) + ->setQuestion($question) + ->setUser($user) + ->setHandles($handles) + ->setAction(PonderConstants::ASKED_LITERAL); + + $panel->appendChild($contentview); + + return $panel->render(); + } + + private function renderObjectLink() { + return phutil_render_tag( + 'a', + array('href' => '/Q' . $this->question->getID()), + "Q". phutil_escape_html($this->question->getID()) + ); + } + +} diff --git a/src/applications/ponder/view/PonderQuestionFeedView.php b/src/applications/ponder/view/PonderQuestionFeedView.php new file mode 100644 index 0000000000..300b49c5e7 --- /dev/null +++ b/src/applications/ponder/view/PonderQuestionFeedView.php @@ -0,0 +1,102 @@ +user = $user; + return $this; + } + + public function setOffset($offset) { + $this->offset = $offset; + return $this; + } + + public function setData($data) { + $this->data = $data; + return $this; + } + + public function setPageSize($pagesize) { + $this->pagesize = $pagesize; + return $this; + } + + public function setHandles($handles) { + $this->handles = $handles; + return $this; + } + + public function setURI($uri, $param) { + $this->uri = $uri; + $this->param = $param; + return $this; + } + + public function render() { + require_celerity_resource('ponder-core-view-css'); + require_celerity_resource('ponder-feed-view-css'); + + $user = $this->user; + $offset = $this->offset; + $data = $this->data; + $handles = $this->handles; + $uri = $this->uri; + $param = $this->param; + $pagesize = $this->pagesize; + + $panel = id(new AphrontPanelView()) + ->setHeader("Popular Questions") + ->addClass("ponder-panel"); + + $panel->addButton( + phutil_render_tag( + 'a', + array( + 'href' => "/ponder/question/ask/", + 'class' => 'green button', + ), + "Ask a question")); + + $pagebuttons = id(new AphrontPagerView()) + ->setPageSize($pagesize) + ->setOffset($offset) + ->setURI($uri, $param); + + $data = $pagebuttons->sliceResults($data); + + + foreach ($data as $question) { + $cur = id(new PonderQuestionSummaryView()) + ->setUser($user) + ->setQuestion($question) + ->setHandles($handles); + $panel->appendChild($cur); + } + + $panel->appendChild($pagebuttons); + return $panel->render(); + } +} diff --git a/src/applications/ponder/view/PonderQuestionSummaryView.php b/src/applications/ponder/view/PonderQuestionSummaryView.php new file mode 100644 index 0000000000..904888ec32 --- /dev/null +++ b/src/applications/ponder/view/PonderQuestionSummaryView.php @@ -0,0 +1,105 @@ +question = $question; + return $this; + } + + public function setHandles($handles) { + $this->handles = $handles; + return $this; + } + + public function setUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + + public function render() { + require_celerity_resource('ponder-feed-view-css'); + + $user = $this->user; + $question = $this->question; + $author_phid = $question->getAuthorPHID(); + $handles = $this->handles; + + $authorlink = $handles[$author_phid] + ->renderLink(); + + $votecount = + '
'. + phutil_escape_html($question->getVoteCount()). + '
'. + 'votes'. + '
'. + '
'; + + $answerclass = "ponder-summary-answers"; + if ($question->getAnswercount() == 0) { + $answerclass .= " ponder-not-answered"; + } + $answercount = + '
'. + phutil_escape_html($question->getAnswerCount()). + '
'. + 'answers'. + '
'. + '
'; + + + $title = + '

'. + phutil_render_tag( + 'a', + array( + "href" => '/Q' . $question->getID(), + ), + phutil_escape_html( + 'Q' . $question->getID() . + ' ' . $question->getTitle() + ) + ) . + '

'; + + $rhs = + ''; + + $summary = + '
'. + $votecount. + $answercount. + $rhs. + '
'; + + + return $summary; + } +} diff --git a/src/applications/ponder/view/PonderUserProfileView.php b/src/applications/ponder/view/PonderUserProfileView.php new file mode 100644 index 0000000000..ab9e815868 --- /dev/null +++ b/src/applications/ponder/view/PonderUserProfileView.php @@ -0,0 +1,159 @@ +user = $user; + return $this; + } + + public function setQuestionOffset($offset) { + $this->questionoffset = $offset; + return $this; + } + + public function setAnswerOffset($offset) { + $this->answeroffset = $offset; + return $this; + } + + public function setQuestions($data) { + $this->questions = $data; + return $this; + } + + public function setAnswers($data) { + $this->answers = $data; + return $this; + } + + public function setPageSize($pagesize) { + $this->pagesize = $pagesize; + return $this; + } + + public function setHandles($handles) { + $this->handles = $handles; + return $this; + } + + public function setURI($uri, $qparam, $aparam) { + $this->uri = $uri; + $this->qparam = $qparam; + $this->aparam = $aparam; + return $this; + } + + public function render() { + require_celerity_resource('ponder-core-view-css'); + require_celerity_resource('ponder-feed-view-css'); + + $user = $this->user; + $qoffset = $this->questionoffset; + $aoffset = $this->answeroffset; + $questions = $this->questions; + $answers = $this->answers; + $handles = $this->handles; + $uri = $this->uri; + $qparam = $this->qparam; + $aparam = $this->aparam; + $pagesize = $this->pagesize; + + + // display questions + $question_panel = id(new AphrontPanelView()) + ->setHeader("Your Questions") + ->addClass("ponder-panel"); + + $question_panel->addButton( + phutil_render_tag( + 'a', + array( + 'href' => "/ponder/question/ask/", + 'class' => 'green button', + ), + "Ask a question")); + + $qpagebuttons = id(new AphrontPagerView()) + ->setPageSize($pagesize) + ->setOffset($qoffset) + ->setURI( + $uri->alter( + $aparam, + $aoffset), + $qparam); + + $questions = $qpagebuttons->sliceResults($questions); + + foreach ($questions as $question) { + $cur = id(new PonderQuestionSummaryView()) + ->setUser($user) + ->setQuestion($question) + ->setHandles($handles); + $question_panel->appendChild($cur); + } + + $question_panel->appendChild($qpagebuttons); + + // display answers + $answer_panel = id(new AphrontPanelView()) + ->setHeader("Your Answers") + ->addClass("ponder-panel") + ->appendChild( + phutil_render_tag( + 'a', + array('id' => 'answers'), + "") + ); + + $apagebuttons = id(new AphrontPagerView()) + ->setPageSize($pagesize) + ->setOffset($aoffset) + ->setURI( + $uri + ->alter( + $qparam, + $qoffset) + ->setFragment("answers"), + $aparam); + + $answers = $apagebuttons->sliceResults($answers); + + foreach ($answers as $answer) { + $cur = id(new PonderAnswerSummaryView()) + ->setUser($user) + ->setAnswer($answer) + ->setHandles($handles); + $answer_panel->appendChild($cur); + } + + $answer_panel->appendChild($apagebuttons); + + return $question_panel->render() . $answer_panel->render(); + } +} diff --git a/src/applications/ponder/view/PonderVotableView.php b/src/applications/ponder/view/PonderVotableView.php new file mode 100644 index 0000000000..72c02f604f --- /dev/null +++ b/src/applications/ponder/view/PonderVotableView.php @@ -0,0 +1,70 @@ +phid = $phid; + return $this; + } + + public function setURI($uri) { + $this->uri = $uri; + return $this; + } + + public function setCount($count) { + $this->count = $count; + return $this; + } + + public function setVote($vote) { + $this->vote = $vote; + return $this; + } + + public function render() { + require_celerity_resource('ponder-vote-css'); + require_celerity_resource('javelin-behavior-ponder-votebox'); + + Javelin::initBehavior( + 'ponder-votebox', + array( + 'nodeid' => $this->phid, + 'vote' => $this->vote, + 'count' => $this->count, + 'uri' => $this->uri + )); + + $content = + '
'. + '
+
'. + $this->renderChildren(). + '
'. + '
'; + + return $content; + } + +} diff --git a/src/applications/search/constants/PhabricatorSearchScope.php b/src/applications/search/constants/PhabricatorSearchScope.php index 240c578737..b174b9f250 100644 --- a/src/applications/search/constants/PhabricatorSearchScope.php +++ b/src/applications/search/constants/PhabricatorSearchScope.php @@ -26,6 +26,7 @@ final class PhabricatorSearchScope { const SCOPE_OPEN_TASKS = 'open-tasks'; const SCOPE_COMMITS = 'commits'; const SCOPE_WIKI = 'wiki'; + const SCOPE_QUESTIONS = 'questions'; public static function getScopeOptions() { return array( @@ -34,6 +35,7 @@ final class PhabricatorSearchScope { self::SCOPE_WIKI => 'Wiki Documents', self::SCOPE_OPEN_REVISIONS => 'Open Revisions', self::SCOPE_COMMITS => 'Commits', + self::SCOPE_QUESTIONS => 'Ponder Questions', ); } diff --git a/src/applications/search/index/PhabricatorSearchAbstractDocument.php b/src/applications/search/index/PhabricatorSearchAbstractDocument.php index 9bea89dda6..b8ea482d04 100644 --- a/src/applications/search/index/PhabricatorSearchAbstractDocument.php +++ b/src/applications/search/index/PhabricatorSearchAbstractDocument.php @@ -37,6 +37,7 @@ final class PhabricatorSearchAbstractDocument { PhabricatorPHIDConstants::PHID_TYPE_TASK => 'Maniphest Tasks', PhabricatorPHIDConstants::PHID_TYPE_WIKI => 'Phriction Documents', PhabricatorPHIDConstants::PHID_TYPE_USER => 'Phabricator Users', + PhabricatorPHIDConstants::PHID_TYPE_QUES => 'Ponder Questions', ) + $more; } diff --git a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php index 6f678a115c..585fa0590b 100644 --- a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php +++ b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php @@ -44,6 +44,11 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { const TYPE_COMMIT_HAS_PROJECT = 15; const TYPE_PROJECT_HAS_COMMIT = 16; + const TYPE_QUESTION_HAS_VOTING_USER = 17; + const TYPE_VOTING_USER_HAS_QUESTION = 18; + const TYPE_ANSWER_HAS_VOTING_USER = 19; + CONST TYPE_VOTING_USER_HAS_ANSWER = 20; + const TYPE_TEST_NO_CYCLE = 9000; public static function getInverse($edge_type) { @@ -71,6 +76,12 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { self::TYPE_COMMIT_HAS_PROJECT => self::TYPE_PROJECT_HAS_COMMIT, self::TYPE_PROJECT_HAS_COMMIT => self::TYPE_COMMIT_HAS_PROJECT, + self::TYPE_QUESTION_HAS_VOTING_USER => + self::TYPE_VOTING_USER_HAS_QUESTION, + self::TYPE_VOTING_USER_HAS_QUESTION => + self::TYPE_QUESTION_HAS_VOTING_USER, + self::TYPE_ANSWER_HAS_VOTING_USER => self::TYPE_VOTING_USER_HAS_ANSWER, + self::TYPE_VOTING_USER_HAS_ANSWER => self::TYPE_ANSWER_HAS_VOTING_USER, ); return idx($map, $edge_type); @@ -98,6 +109,8 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { PhabricatorPHIDConstants::PHID_TYPE_TOBJ => 'HarbormasterObject', PhabricatorPHIDConstants::PHID_TYPE_BLOG => 'PhameBlog', PhabricatorPHIDConstants::PHID_TYPE_POST => 'PhamePost', + PhabricatorPHIDConstants::PHID_TYPE_QUES => 'PonderQuestion', + PhabricatorPHIDConstants::PHID_TYPE_ANSW => 'PonderAnswer', ); $class = idx($class_map, $phid_type); diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php index c626e9e055..0073e565b3 100644 --- a/src/infrastructure/markup/PhabricatorMarkupEngine.php +++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php @@ -330,6 +330,11 @@ final class PhabricatorMarkupEngine { } + public static function newPonderMarkupEngine(array $options = array()) { + return self::newMarkupEngine($options); + } + + /** * @task engine */ @@ -406,6 +411,8 @@ final class PhabricatorMarkupEngine { $rules[] = new PhabricatorRemarkupRuleManiphest(); $rules[] = new PhabricatorRemarkupRulePaste(); + $rules[] = new PonderRuleQuestion(); + if ($options['macros']) { $rules[] = new PhabricatorRemarkupRuleImageMacro(); } diff --git a/src/infrastructure/markup/rule/PonderRuleQuestion.php b/src/infrastructure/markup/rule/PonderRuleQuestion.php new file mode 100644 index 0000000000..8f017e4386 --- /dev/null +++ b/src/infrastructure/markup/rule/PonderRuleQuestion.php @@ -0,0 +1,29 @@ + 'db', 'name' => 'fact', ), + 'db.ponder' => array( + 'type' => 'db', + 'name' => 'ponder', + ), '0000.legacy.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('0000.legacy.sql'), @@ -952,6 +956,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'type' => 'sql', 'name' => $this->getPatchPath('fact-raw.sql'), ), + 'ponder.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('ponder.sql') + ), ); } diff --git a/webroot/rsrc/css/application/ponder/core.css b/webroot/rsrc/css/application/ponder/core.css new file mode 100644 index 0000000000..327f20c83b --- /dev/null +++ b/webroot/rsrc/css/application/ponder/core.css @@ -0,0 +1,25 @@ +/** + * @provides ponder-core-view-css + */ + +.ponder-primary-pane { + margin: 0 0 0 2em; + padding-bottom: 2em; + max-width: 800px; +} + +.ponder-panel { + max-width: 800px; + font-family : antiqua, verdana, arial; +} + +.ponder-panel h1 { + padding-bottom: 8px; + margin-bottom: 8px; + font-size : 1.5em; +} + +.ponder-panel .aphront-form-view { + border : none; + background : none; +} diff --git a/webroot/rsrc/css/application/ponder/feed.css b/webroot/rsrc/css/application/ponder/feed.css new file mode 100644 index 0000000000..eb031fdef9 --- /dev/null +++ b/webroot/rsrc/css/application/ponder/feed.css @@ -0,0 +1,74 @@ +/** + * @provides ponder-feed-view-css + */ + +.ponder-question-summary { + width : 100%; + background : #DFDFE3; + float : left; + clear : both; + margin-top : 1px; + padding : 1px; +} + +.ponder-answer-summary { + width : 100%; + background : #DFDFE3; + float : left; + clear : both; + margin-top : 1px; + padding : 1px; +} + +.ponder-summary-votes { + width : 50px; + height : 36pt; + font-size : 18pt; + text-align : center; + background : #EEE; + border : 1px solid #BBB; + float : left; + margin : 2px; + padding-top : 2px; +} + +.ponder-summary-answers { + width : 50px; + height : 36pt; + font-size : 18pt; + text-align : center; + background : #EEE; + border : 1px solid #BBB; + float : left; + margin : 2px; + padding-top : 2px; +} + +.ponder-question-label { + font-size : 6pt; +} + +h2.ponder-question-title { + font-size : 12pt; + margin : 2px; + padding : 0; +} + +h2.ponder-answer-title { + font-size : 12pt; + margin : 2px; + padding : 0; +} + +.ponder-metadata { + padding-left : 5px; + width : 650px; + float : left; +} + +.ponder-small-metadata { + font-size : 7.5pt; + color : #555; + margin : 0; + text-align : right; +} diff --git a/webroot/rsrc/css/application/ponder/post.css b/webroot/rsrc/css/application/ponder/post.css new file mode 100644 index 0000000000..30155ec4b8 --- /dev/null +++ b/webroot/rsrc/css/application/ponder/post.css @@ -0,0 +1,34 @@ +/** + * @provides ponder-post-css + */ + +.ponder-post-list { + max-width: 1162px; +} + +.ponder-add-answer-panel { + max-width: 1162px; +} + +.ponder-post-list .anchor-target { + background-color: #ffffdd; + border-color: #ffff00; +} + +.ponder-post-core .phabricator-remarkup .remarkup-code-block { + width: 88ex; + width: 81ch; +} + +.phabricator-transaction-view .ponder-question { + border-color: #777; +} + +.phabricator-transaction-detail .ponder-question { + border-color: #777; +} + + +.phabricator-transaction-view .ponder-answer { +/* border-color: #203791; */ +} diff --git a/webroot/rsrc/css/application/ponder/vote.css b/webroot/rsrc/css/application/ponder/vote.css new file mode 100644 index 0000000000..6d09e24d42 --- /dev/null +++ b/webroot/rsrc/css/application/ponder/vote.css @@ -0,0 +1,74 @@ +/** + * @provides ponder-vote-css + */ + +.ponder-votable { + min-height : 120px; +} + +.ponder-votable .phabricator-transaction-detail { + min-height : 90px; +} + +.ponder-votebox { + float : left; + width : 32px; + height : 60px; + margin-top : 56px; + margin-left : 10px; +} + +.ponder-upbutton { + border : none; + padding : 0; + margin : 0; + width : 32px; + height : 13px; +} + +.ponder-downbutton { + border : none; + padding : 0; + margin : 0; + width : 32px; + height : 13px; +} + +.ponder-votecount { + width : 32px; + height : 22pt; + padding : 0; + margin : 0; + overflow : visible; + text-align : center; + font-size : 15pt; +} + +.ponder-upbutton:hover { + cursor : pointer; +} + +.ponder-downbutton:hover { + cursor : pointer; +} + +.ponder-votable-bottom { + clear : both; +} + +.ponder-upbutton { + background : url(/rsrc/image/application/ponder/upvote.png) 0 -13px no-repeat; +} + +.ponder-upbutton.ponder-vote-up { + background : url(/rsrc/image/application/ponder/upvote.png) 0 0 no-repeat; +} + +.ponder-downbutton { + background : url(/rsrc/image/application/ponder/downvote.png) + 0 -13px no-repeat; +} + +.ponder-downbutton.ponder-vote-down { + background : url(/rsrc/image/application/ponder/downvote.png) 0 0 no-repeat; +} diff --git a/webroot/rsrc/image/app/app_ponder.png b/webroot/rsrc/image/app/app_ponder.png new file mode 100644 index 0000000000000000000000000000000000000000..548f5162ebd177967c3f54aa70645c93748c08bd GIT binary patch literal 8661 zcmeAS@N?(olHy`uVBq!ia0vp^dLYcf1|-9GYMTQo#^NA%Cx&(BWL^R}Y)RhkE)4%c zaKYZ?lYt_f1s;*b3=G`DAk4@xYmNj^kiEpy*OmPa2Meo)L3Zi4eg*~w22U5qkPKEv zJH>u@Gqng_f1sKe+sQ11s+^m#LBb%$;mkm{g7BaM_#8eN=u+N6;M&uJzzw5L7$G4LJ0pA>a7BS9 gXf419@iJ|_x%!Gs@Riqzopr0AB+_>Hq)$ literal 0 HcmV?d00001 diff --git a/webroot/rsrc/image/application/ponder/downvote.png b/webroot/rsrc/image/application/ponder/downvote.png new file mode 100644 index 0000000000000000000000000000000000000000..7272a6645cb1c2d32bc9057da1c0585c3e159b2e GIT binary patch literal 929 zcmV;S177@zP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyl9 z6e|iQM3s{O00SXOL_t(Y$Gw&@Y+F?nhQI&DlEo0I8v}`=G)3*wr4kDhVlAy;K^-FK z0?MF1IhMjQLGd-aKWpPx}<`0c-Y11)fX>r{dhp6g@s!)=(Sf^r?P%z zxPpU55|FwSp;VnvqL+6AAVl-|^VJrk%2u|;#V?ob@BmRABxEaVx3P_>Jm0uY@?&tV z*ZchEQ3qzEEiK+tU;pd`>yo2%(x9xvg~%7Pqflfo_tVrjo;|C7b=WaJ)J~ticiEn~ zD{;=oQVx)nC=*#`h?_|tNnYRE`}FC!dt@S8TKxKTm!1{7CSyUY$L_{!uGDD ztL=8<;4n&()(r3pICUKOnVOR1G)*O`xwW-6ei+qix0h`{0IDVCM^UBNlVLoTRBN{z zlMkbMeM9|ze+Rgv%*K%@WlZMV2X3_6jp>I`t#qstdvPY8E+wI2j z45O8mFCI!-2Eslfx!gm;sMBdqG>pQe1UjAO$CBQX^wYjS+4WoYnSOT2~Y^g}G00000NkvXXu0mjf DETOO2 literal 0 HcmV?d00001 diff --git a/webroot/rsrc/image/application/ponder/upvote.png b/webroot/rsrc/image/application/ponder/upvote.png new file mode 100644 index 0000000000000000000000000000000000000000..cb543a214156e71419854a1e6f701217b480e2f3 GIT binary patch literal 916 zcmV;F18e+=P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyl9 z6e%dd61MdK00R_BL_t(Y$F-I}j8s(=#=qa4KM;|^n22OREGSL3p+XZ9jI<;)G;V<* z?#xh_A!tkn&1RQ1F>GvB-w-!65)v&;tn6sqU_nE-G|(_%lNAze)W*4=#e4Ji&CKp* zXV|&jx$mBHzWbeX?)T6H>$TTEq!M0v*Yb~`nyr?k@1H+k{b$@*jO$Q)^W%g6#p+!v zUI0nLvM@Z6xb(;7%Gy)_yivOr=*81kTnh@4Y+Di{3u0MF%v+V^&CRd>-Zub8j$At& zsN4}gpSpO!wP4v2NRJ>~-q=|B{iOlaYV$L;A4saEDXfTKqj z4ngI%6fb7SMpE{<$Sou?2u4Q2W?ez;XWh?#*)xDz?VGa@?}2Qmoi>zhG(@p7E}GZ| zp%p{xS@)CPjsT7y|L&ONmgK8Q_5hueR1qs90vQw5Wg6XX{kIYC6K}1aGQO{>{A~Bj(YHIdZhbMC z7&RI<4oSK#>0)+kk5jS&8|#wVNz&fC7&V(qXJd@_vIfiYPc$%cx&T&yR+6-L7NdH7 z@mL6z{Fp+fBqjaoo&+fzISo}0000