From 51418900f70ee62da7fe97eeeec89ab6fc5dd0db Mon Sep 17 00:00:00 2001 From: Bob Trahan Date: Thu, 12 Apr 2012 13:09:04 -0700 Subject: [PATCH] Phame V1 - Phabricator blogging software Summary: 'cuz we need to be phamous! V1 feature set - posts -- standard thing you'd expect - a title and a remarkup-powered body and... -- "phame" title - a short string that can be used to reference the story. this gets auto-updated when you mess with the title. -- configuration - for now, do you want Facebook, Disqus or no comments? this is a per-post thing but feeds from an instance-wide configuration Please do toss out any must have features or changes. Test Plan: played around with this bad boy like whoa Reviewers: epriestley Reviewed By: epriestley CC: aran, vrana Maniphest Tasks: T1111 Differential Revision: https://secure.phabricator.com/D2202 --- conf/default.conf.php | 10 + resources/sql/patches/132.phame.sql | 18 ++ src/__celerity_resource_map__.php | 13 + src/__phutil_library_map__.php | 26 ++ ...AphrontDefaultApplicationConfiguration.php | 22 ++ .../markup/engine/PhabricatorMarkupEngine.php | 7 + .../phame/controller/base/PhameController.php | 91 ++++++ .../phame/controller/base/__init__.php | 18 ++ .../post/delete/PhamePostDeleteController.php | 81 ++++++ .../phame/controller/post/delete/__init__.php | 20 ++ .../post/edit/PhamePostEditController.php | 269 ++++++++++++++++++ .../phame/controller/post/edit/__init__.php | 31 ++ .../list/base/PhamePostListBaseController.php | 127 +++++++++ .../controller/post/list/base/__init__.php | 20 ++ .../list/drafts/PhameDraftListController.php | 29 ++ .../controller/post/list/drafts/__init__.php | 12 + .../list/posts/PhamePostListController.php | 29 ++ .../controller/post/list/posts/__init__.php | 12 + .../preview/PhamePostPreviewController.php | 51 ++++ .../controller/post/preview/__init__.php | 17 ++ .../post/view/PhamePostViewController.php | 151 ++++++++++ .../phame/controller/post/view/__init__.php | 21 ++ .../phame/query/post/PhamePostQuery.php | 79 +++++ .../phame/query/post/__init__.php | 15 + .../phame/storage/base/PhameDAO.php | 28 ++ .../phame/storage/base/__init__.php | 12 + .../phame/storage/post/PhamePost.php | 106 +++++++ .../phame/storage/post/__init__.php | 19 ++ .../view/postdetail/PhamePostDetailView.php | 221 ++++++++++++++ .../phame/view/postdetail/__init__.php | 21 ++ .../phame/view/postlist/PhamePostListView.php | 137 +++++++++ .../phame/view/postlist/__init__.php | 19 ++ .../constants/PhabricatorPHIDConstants.php | 1 + src/docs/userguide/phame.diviner | 18 ++ .../application/phame/phame-post-preview.js | 81 ++++++ 35 files changed, 1832 insertions(+) create mode 100644 resources/sql/patches/132.phame.sql create mode 100644 src/applications/phame/controller/base/PhameController.php create mode 100644 src/applications/phame/controller/base/__init__.php create mode 100644 src/applications/phame/controller/post/delete/PhamePostDeleteController.php create mode 100644 src/applications/phame/controller/post/delete/__init__.php create mode 100644 src/applications/phame/controller/post/edit/PhamePostEditController.php create mode 100644 src/applications/phame/controller/post/edit/__init__.php create mode 100644 src/applications/phame/controller/post/list/base/PhamePostListBaseController.php create mode 100644 src/applications/phame/controller/post/list/base/__init__.php create mode 100644 src/applications/phame/controller/post/list/drafts/PhameDraftListController.php create mode 100644 src/applications/phame/controller/post/list/drafts/__init__.php create mode 100644 src/applications/phame/controller/post/list/posts/PhamePostListController.php create mode 100644 src/applications/phame/controller/post/list/posts/__init__.php create mode 100644 src/applications/phame/controller/post/preview/PhamePostPreviewController.php create mode 100644 src/applications/phame/controller/post/preview/__init__.php create mode 100644 src/applications/phame/controller/post/view/PhamePostViewController.php create mode 100644 src/applications/phame/controller/post/view/__init__.php create mode 100644 src/applications/phame/query/post/PhamePostQuery.php create mode 100644 src/applications/phame/query/post/__init__.php create mode 100644 src/applications/phame/storage/base/PhameDAO.php create mode 100644 src/applications/phame/storage/base/__init__.php create mode 100644 src/applications/phame/storage/post/PhamePost.php create mode 100644 src/applications/phame/storage/post/__init__.php create mode 100644 src/applications/phame/view/postdetail/PhamePostDetailView.php create mode 100644 src/applications/phame/view/postdetail/__init__.php create mode 100644 src/applications/phame/view/postlist/PhamePostListView.php create mode 100644 src/applications/phame/view/postlist/__init__.php create mode 100644 src/docs/userguide/phame.diviner create mode 100644 webroot/rsrc/js/application/phame/phame-post-preview.js diff --git a/conf/default.conf.php b/conf/default.conf.php index 8d2218557a..c4e1977102 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -489,6 +489,16 @@ return array( // The Phabricator "Client Secret" to use for Phabricator API access. 'phabricator.application-secret' => null, +// -- Disqus Comments ------------------------------------------------------- // + + // Should Phame users have Disqus comment widget, and if so what's the + // website shortname to use? For example, secure.phabricator.org uses + // "phabricator", which we registered with Disqus. If you aren't familiar + // with Disqus, see: + // Disqus quick start guide - http://docs.disqus.com/help/4/ + // Information on shortnames - http://docs.disqus.com/help/68/ + 'disqus.shortname' => null, + // -- Recaptcha ------------------------------------------------------------- // // Is Recaptcha enabled? If disabled, captchas will not appear. You should diff --git a/resources/sql/patches/132.phame.sql b/resources/sql/patches/132.phame.sql new file mode 100644 index 0000000000..04e15a45bb --- /dev/null +++ b/resources/sql/patches/132.phame.sql @@ -0,0 +1,18 @@ +CREATE DATABASE IF NOT EXISTS `phabricator_phame` COLLATE utf8_general_ci; + +CREATE TABLE `phabricator_phame`.`phame_post` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + `phid` VARCHAR(64) BINARY NOT NULL COLLATE utf8_bin, + `bloggerPHID` VARCHAR(64) BINARY NOT NULL COLLATE utf8_bin, + `title` VARCHAR(255) NOT NULL, + `phameTitle` VARCHAR(64) NOT NULL, + `body` LONGTEXT COLLATE utf8_general_ci, + `visibility` INT UNSIGNED NOT NULL DEFAULT 0, + `configData` LONGTEXT COLLATE utf8_general_ci, + `datePublished` INT UNSIGNED NOT NULL, + `dateCreated` INT UNSIGNED NOT NULL, + `dateModified` INT UNSIGNED NOT NULL, + KEY `bloggerPosts` (`bloggerPHID`, `visibility`, `datePublished`, `id`), + UNIQUE KEY `phid` (`phid`), + UNIQUE KEY `phameTitle` (`bloggerPHID`, `phameTitle`) +) ENGINE=InnoDB; diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index be745a316a..4b7cc40e81 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -1281,6 +1281,19 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/core/behavior-watch-anchor.js', ), + 'javelin-behavior-phame-post-preview' => + array( + 'uri' => '/res/ac4c503a/rsrc/js/application/phame/phame-post-preview.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'phabricator-shaped-request', + ), + 'disk' => '/rsrc/js/application/phame/phame-post-preview.js', + ), 'javelin-behavior-phriction-document-preview' => array( 'uri' => '/res/f1665ecd/rsrc/js/application/phriction/phriction-document-preview.js', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5c91811317..8934e7f9a1 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -929,6 +929,19 @@ phutil_register_library_map(array( 'PhabricatorXHProfProfileSymbolView' => 'applications/xhprof/view/symbol', 'PhabricatorXHProfProfileTopLevelView' => 'applications/xhprof/view/toplevel', 'PhabricatorXHProfProfileView' => 'applications/xhprof/view/base', + 'PhameController' => 'applications/phame/controller/base', + 'PhameDAO' => 'applications/phame/storage/base', + 'PhameDraftListController' => 'applications/phame/controller/post/list/drafts', + 'PhamePost' => 'applications/phame/storage/post', + 'PhamePostDeleteController' => 'applications/phame/controller/post/delete', + 'PhamePostDetailView' => 'applications/phame/view/postdetail', + 'PhamePostEditController' => 'applications/phame/controller/post/edit', + 'PhamePostListBaseController' => 'applications/phame/controller/post/list/base', + 'PhamePostListController' => 'applications/phame/controller/post/list/posts', + 'PhamePostListView' => 'applications/phame/view/postlist', + 'PhamePostPreviewController' => 'applications/phame/controller/post/preview', + 'PhamePostQuery' => 'applications/phame/query/post', + 'PhamePostViewController' => 'applications/phame/controller/post/view', 'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/monthyearexpiry', 'PhortuneStripeBaseController' => 'applications/phortune/stripe/controller/base', 'PhortuneStripePaymentFormView' => 'applications/phortune/stripe/view/paymentform', @@ -1743,6 +1756,19 @@ phutil_register_library_map(array( 'PhabricatorXHProfProfileSymbolView' => 'PhabricatorXHProfProfileView', 'PhabricatorXHProfProfileTopLevelView' => 'PhabricatorXHProfProfileView', 'PhabricatorXHProfProfileView' => 'AphrontView', + 'PhameController' => 'PhabricatorController', + 'PhameDAO' => 'PhabricatorLiskDAO', + 'PhameDraftListController' => 'PhamePostListBaseController', + 'PhamePost' => 'PhameDAO', + 'PhamePostDeleteController' => 'PhameController', + 'PhamePostDetailView' => 'AphrontView', + 'PhamePostEditController' => 'PhameController', + 'PhamePostListBaseController' => 'PhameController', + 'PhamePostListController' => 'PhamePostListBaseController', + 'PhamePostListView' => 'AphrontView', + 'PhamePostPreviewController' => 'PhameController', + 'PhamePostQuery' => 'PhabricatorOffsetPagedQuery', + 'PhamePostViewController' => 'PhameController', 'PhortuneMonthYearExpiryControl' => 'AphrontFormControl', 'PhortuneStripeBaseController' => 'PhabricatorController', 'PhortuneStripePaymentFormView' => 'AphrontView', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 4b1f970725..e74a089f8a 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -385,6 +385,28 @@ class AphrontDefaultApplicationConfiguration 'diff/(?P\d+)/' => 'PhrictionDiffController', ), + '/phame/' => array( + '' => 'PhamePostListController', + 'post/' => array( + '' => 'PhamePostListController', + 'delete/(?P[^/]+)/' => 'PhamePostDeleteController', + 'edit/(?P[^/]+)/' => 'PhamePostEditController', + 'new/' => 'PhamePostEditController', + 'preview/' => 'PhamePostPreviewController', + 'view/(?P[^/]+)/' => 'PhamePostViewController', + ), + 'draft/' => array( + '' => 'PhameDraftListController', + 'new/' => 'PhamePostEditController', + ), + 'posts/' => array( + '' => 'PhamePostListController', + '(?P\w+)/' => 'PhamePostListController', + '(?P\w+)/(?P.+/)' + => 'PhamePostViewController', + ), + ), + '/calendar/' => array( '' => 'PhabricatorCalendarBrowseController', ), diff --git a/src/applications/markup/engine/PhabricatorMarkupEngine.php b/src/applications/markup/engine/PhabricatorMarkupEngine.php index bcdc2a8170..5ca97efd87 100644 --- a/src/applications/markup/engine/PhabricatorMarkupEngine.php +++ b/src/applications/markup/engine/PhabricatorMarkupEngine.php @@ -49,6 +49,13 @@ class PhabricatorMarkupEngine { )); } + public static function newPhameMarkupEngine() { + return self::newMarkupEngine(array( + 'macros' => false, + )); + } + + public static function newDifferentialMarkupEngine(array $options = array()) { return self::newMarkupEngine(array( 'custom-inline' => PhabricatorEnv::getEnvConfig( diff --git a/src/applications/phame/controller/base/PhameController.php b/src/applications/phame/controller/base/PhameController.php new file mode 100644 index 0000000000..67ce808a90 --- /dev/null +++ b/src/applications/phame/controller/base/PhameController.php @@ -0,0 +1,91 @@ +showSideNav = (bool) $value; + return $this; + } + private function showSideNav() { + return $this->showSideNav; + } + + public function buildStandardPageResponse($view, array $data) { + + $page = $this->buildStandardPageView(); + + $page->setApplicationName('Phame'); + $page->setBaseURI('/phame/'); + $page->setTitle(idx($data, 'title')); + $page->setGlyph("\xe2\x9c\xa9"); + + $tabs = array( + 'help' => array( + 'name' => 'Help', + 'href' => + PhabricatorEnv::getDoclink('article/Phame_User_Guide.html'), + ), + ); + $page->setTabs($tabs, idx($data, 'tab')); + if ($this->showSideNav()) { + $nav = $this->renderSideNavFilterView($this->getSideNavFilter()); + $nav->appendChild($view); + $page->appendChild($nav); + } else { + $page->appendChild($view); + } + + $response = new AphrontWebpageResponse(); + return $response->setContent($page->render()); + } + + private function renderSideNavFilterView($filter) { + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI('/phame/')); + $nav->addLabel('Drafts'); + $nav->addFilter('post/new', + 'New Draft'); + $nav->addFilter('draft', + 'My Drafts'); + $nav->addSpacer(); + $nav->addLabel('Posts'); + $nav->addFilter('post', + 'My Posts'); + foreach ($this->getSideNavExtraPostFilters() as $post_filter) { + $nav->addFilter($post_filter['key'], + $post_filter['name']); + } + + $nav->selectFilter($filter, 'post'); + + return $nav; + } + + protected function getSideNavExtraPostFilters() { + return array(); + } + protected function getSideNavFilter() { + return 'post'; + } + +} diff --git a/src/applications/phame/controller/base/__init__.php b/src/applications/phame/controller/base/__init__.php new file mode 100644 index 0000000000..1e2554a34f --- /dev/null +++ b/src/applications/phame/controller/base/__init__.php @@ -0,0 +1,18 @@ +phid = $phid; + return $this; + } + private function getPostPHID() { + return $this->phid; + } + + protected function getSideNavFilter() { + return 'post/delete/'.$this->getPostPHID(); + } + + protected function getSideNavExtraPostFilters() { + $filters = array( + array('key' => $this->getSideNavFilter(), + 'name' => 'Delete Post') + ); + + return $filters; + } + + public function willProcessRequest(array $data) { + $phid = $data['phid']; + $this->setPostPHID($phid); + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + $post = id(new PhamePost())->loadOneWhere( + 'phid = %s', + $this->getPostPHID()); + if (empty($post)) { + return new Aphront404Response(); + } + if ($post->getBloggerPHID() != $user->getPHID()) { + return new Aphront403Response(); + } + $edit_uri = $post->getEditURI(); + + if ($request->isFormPost()) { + $post->delete(); + return id(new AphrontRedirectResponse())->setURI('/phame/?deleted'); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($user) + ->setTitle('Delete post?') + ->appendChild('Really delete this post? It will be gone forever.') + ->addSubmitButton('Delete') + ->addCancelButton($edit_uri); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } +} diff --git a/src/applications/phame/controller/post/delete/__init__.php b/src/applications/phame/controller/post/delete/__init__.php new file mode 100644 index 0000000000..83a53fdff6 --- /dev/null +++ b/src/applications/phame/controller/post/delete/__init__.php @@ -0,0 +1,20 @@ +phid = $phid; + return $this; + } + private function getPostPHID() { + return $this->phid; + } + private function setIsPostEdit($is_post_edit) { + $this->isPostEdit = $is_post_edit; + return $this; + } + private function isPostEdit() { + return $this->isPostEdit; + } + + protected function getSideNavFilter() { + if ($this->isPostEdit()) { + $filter = 'post/edit/'.$this->getPostPHID(); + } else { + $filter = 'post/new'; + } + return $filter; + } + protected function getSideNavExtraPostFilters() { + if ($this->isPostEdit()) { + $filters = array( + array('key' => 'post/edit/'.$this->getPostPHID(), + 'name' => 'Edit Post') + ); + } else { + $filters = array(); + } + + return $filters; + } + + public function willProcessRequest(array $data) { + $phid = idx($data, 'phid'); + $this->setPostPHID($phid); + $this->setIsPostEdit((bool) $phid); + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + $e_phame_title = null; + $e_title = null; + $errors = array(); + + if ($this->isPostEdit()) { + $post = id(new PhamePost())->loadOneWhere( + 'phid = %s', + $this->getPostPHID()); + if (empty($post)) { + return new Aphront404Response(); + } + if ($post->getBloggerPHID() != $user->getPHID()) { + return new Aphront403Response(); + } + $cancel_uri = $post->getViewURI($user->getUsername()); + $submit_button = 'Save Changes'; + $delete_button = javelin_render_tag( + 'a', + array( + 'href' => $post->getDeleteURI(), + 'class' => 'grey button', + 'sigil' => 'workflow', + ), + 'Delete Post'); + $page_title = 'Edit Post'; + } else { + $post = id(new PhamePost()) + ->setBloggerPHID($user->getPHID()) + ->setVisibility(PhamePost::VISIBILITY_DRAFT); + $cancel_uri = '/phame'; + $submit_button = 'Create Post'; + $delete_button = null; + $page_title = 'Create Post'; + } + + if ($request->isFormPost()) { + $saved = true; + $visibility = $request->getInt('visibility'); + $comments = $request->getStr('comments_widget'); + $data = array('comments_widget' => $comments); + $phame_title = $request->getStr('phame_title'); + $phame_title = PhabricatorSlug::normalize($phame_title); + $title = $request->getStr('title'); + $post->setTitle($title); + $post->setPhameTitle($phame_title); + $post->setBody($request->getStr('body')); + $post->setVisibility($visibility); + $post->setConfigData($data); + // only publish once...! + if ($visibility == PhamePost::VISIBILITY_PUBLISHED) { + if (!$post->getDatePublished()) { + $post->setDatePublished(time()); + } + // this is basically a cast of null to 0 if its a new post + } else if (!$post->getDatePublished()) { + $post->setDatePublished(0); + } + if ($phame_title == '/') { + $errors[] = 'Phame title must be nonempty.'; + $e_phame_title = 'Required'; + } + if (empty($title)) { + $errors[] = 'Title must be nonempty.'; + $e_title = 'Required'; + } + if (empty($errors)) { + try { + $post->save(); + } catch (AphrontQueryDuplicateKeyException $e) { + $saved = false; + $e_phame_title = 'Not Unique'; + $errors[] = 'Another post already uses this slug. '. + 'Each post must have a unique slug.'; + } + } else { + $saved = false; + } + + if ($saved) { + $uri = new PhutilURI($post->getViewURI($user->getUsername())); + return id(new AphrontRedirectResponse()) + ->setURI($uri); + } + } + + $panel = new AphrontPanelView(); + $panel->setHeader($page_title); + $panel->setWidth(AphrontPanelView::WIDTH_FULL); + if ($delete_button) { + $panel->addButton($delete_button); + } + + $remarkup_reference = phutil_render_tag( + 'a', + array( + 'href' => + PhabricatorEnv::getDoclink('article/Remarkup_Reference.html'), + 'tabindex' => '-1', + 'target' => '_blank', + ), + 'Formatting Reference'); + + $form = id(new AphrontFormView()) + ->setUser($user) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Title') + ->setName('title') + ->setValue($post->getTitle()) + ->setID('post-title') + ->setError($e_title) + ) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Phame Title') + ->setName('phame_title') + ->setValue(rtrim($post->getPhameTitle(), '/')) + ->setID('post-phame-title') + ->setCaption('Up to 64 alphanumeric characters '. + 'with underscores for spaces. '. + 'Formatting is enforced.') + ->setError($e_phame_title) + ) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel('Body') + ->setName('body') + ->setValue($post->getBody()) + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) + ->setEnableDragAndDropFileUploads(true) + ->setID('post-body') + ->setCaption($remarkup_reference) + ) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel('Visibility') + ->setName('visibility') + ->setValue($post->getVisibility()) + ->setOptions(PhamePost::getVisibilityOptionsForSelect()) + ) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel('Comments Widget') + ->setName('comments_widget') + ->setvalue($post->getCommentsWidget()) + ->setOptions($post->getCommentsWidgetOptionsForSelect()) + ) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton($cancel_uri) + ->setValue($submit_button) + ); + + $panel->appendChild($form); + + $preview_panel = + '
+
+ Post Preview +
+
+
+ Loading preview... +
+
+
'; + + Javelin::initBehavior( + 'phame-post-preview', + array( + 'preview' => 'post-preview', + 'body' => 'post-body', + 'title' => 'post-title', + 'phame_title' => 'post-phame-title', + 'uri' => '/phame/post/preview/', + )); + + if ($errors) { + $error_view = id(new AphrontErrorView()) + ->setTitle('Errors saving post.') + ->setErrors($errors); + } else { + $error_view = null; + } + + $this->setShowSideNav(true); + return $this->buildStandardPageResponse( + array( + $error_view, + $panel, + $preview_panel, + ), + array( + 'title' => $page_title, + )); + } +} diff --git a/src/applications/phame/controller/post/edit/__init__.php b/src/applications/phame/controller/post/edit/__init__.php new file mode 100644 index 0000000000..14b6c8e65e --- /dev/null +++ b/src/applications/phame/controller/post/edit/__init__.php @@ -0,0 +1,31 @@ +bloggerName = $blogger_name; + return $this; + } + private function getBloggerName() { + return $this->bloggerName; + } + + protected function getSideNavExtraPostFilters() { + if ($this->isDraft() || !$this->getBloggerName()) { + return array(); + } + + return + array(array('key' => $this->getSideNavFilter(), + 'name' => 'Posts by '.$this->getBloggerName())); + } + + protected function getSideNavFilter() { + if ($this->getBloggerName()) { + $filter = 'posts/'.$this->getBloggerName(); + } else if ($this->isDraft()) { + $filter = 'draft'; + } else { + $filter = 'posts'; + } + return $filter; + } + + private function isDraft() { + return (bool) $this->isDraft; + } + protected function setIsDraft($is_draft) { + $this->isDraft = $is_draft; + return $this; + } + + public function willProcessRequest(array $data) { + $this->setBloggerName(idx($data, 'bloggername')); + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + $pager = new AphrontPagerView(); + $page_size = 50; + $pager->setURI($request->getRequestURI(), 'offset'); + $pager->setPageSize($page_size); + $pager->setOffset($request->getInt('offset')); + + if ($this->getBloggerName()) { + $blogger = id(new PhabricatorUser())->loadOneWhere( + 'username = %s', + $this->getBloggerName()); + if (!$blogger) { + return new Aphront404Response(); + } + $page_title = 'Posts by '.$this->getBloggerName(); + if ($blogger->getPHID() == $user->getPHID()) { + $actions = array('view', 'edit'); + } else { + $actions = array('view'); + } + $this->setShowSideNav(false); + } else { + $blogger = $user; + $page_title = 'Posts by '.$user->getUserName(); + $actions = array('view', 'edit'); + $this->setShowSideNav(true); + } + $phid = $blogger->getPHID(); + // user gets to see their own unpublished stuff + if ($phid == $user->getPHID() && $this->isDraft()) { + $post_visibility = PhamePost::VISIBILITY_DRAFT; + } else { + $post_visibility = PhamePost::VISIBILITY_PUBLISHED; + } + $query = new PhamePostQuery(); + $query->withBloggerPHID($phid); + $query->withVisibility($post_visibility); + $posts = $query->executeWithPager($pager); + $bloggers = array($blogger->getPHID() => $blogger); + + $panel = id(new PhamePostListView()) + ->setUser($user) + ->setBloggers($bloggers) + ->setPosts($posts) + ->setActions($actions) + ->setDraftList($this->isDraft()); + + return $this->buildStandardPageResponse( + array( + $panel, + $pager + ), + array( + 'title' => $page_title, + )); + } +} diff --git a/src/applications/phame/controller/post/list/base/__init__.php b/src/applications/phame/controller/post/list/base/__init__.php new file mode 100644 index 0000000000..f9d36aaec9 --- /dev/null +++ b/src/applications/phame/controller/post/list/base/__init__.php @@ -0,0 +1,20 @@ +setIsDraft(true); + return parent::processRequest(); + } +} diff --git a/src/applications/phame/controller/post/list/drafts/__init__.php b/src/applications/phame/controller/post/list/drafts/__init__.php new file mode 100644 index 0000000000..4d94f43d21 --- /dev/null +++ b/src/applications/phame/controller/post/list/drafts/__init__.php @@ -0,0 +1,12 @@ +setIsDraft(false); + return parent::processRequest(); + } +} diff --git a/src/applications/phame/controller/post/list/posts/__init__.php b/src/applications/phame/controller/post/list/posts/__init__.php new file mode 100644 index 0000000000..53542d16ab --- /dev/null +++ b/src/applications/phame/controller/post/list/posts/__init__.php @@ -0,0 +1,12 @@ +getRequest(); + $user = $request->getUser(); + $body = $request->getStr('body'); + $title = $request->getStr('title'); + $phame_title = $request->getStr('phame_title'); + + $post = id(new PhamePost()) + ->setBody($body) + ->setTitle($title) + ->setPhameTitle($phame_title) + ->setDateModified(time()); + + $post_html = id(new PhamePostDetailView()) + ->setUser($user) + ->setBlogger($user) + ->setPost($post) + ->setIsPreview(true) + ->render(); + + return id(new AphrontAjaxResponse())->setContent($post_html); + } +} diff --git a/src/applications/phame/controller/post/preview/__init__.php b/src/applications/phame/controller/post/preview/__init__.php new file mode 100644 index 0000000000..f484d2775f --- /dev/null +++ b/src/applications/phame/controller/post/preview/__init__.php @@ -0,0 +1,17 @@ +postPHID = $post_phid; + return $this; + } + private function getPostPHID() { + return $this->postPHID; + } + private function setPhameTitle($phame_title) { + $this->phameTitle = $phame_title; + return $this; + } + private function getPhameTitle() { + return $this->phameTitle; + } + private function setBloggerName($blogger_name) { + $this->bloggerName = $blogger_name; + return $this; + } + private function getBloggerName() { + return $this->bloggerName; + } + + protected function getSideNavFilter() { + $filter = 'post/view/'.$this->getPostPHID(); + return $filter; + } + protected function getSideNavExtraPostFilters() { + $filters = array( + array('key' => $this->getSideNavFilter(), + 'name' => $this->getPhameTitle()) + ); + return $filters; + } + + public function willProcessRequest(array $data) { + $this->setPostPHID(idx($data, 'phid')); + $this->setPhameTitle(idx($data, 'phametitle')); + $this->setBloggerName(idx($data, 'bloggername')); + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + $post_phid = null; + + if ($this->getPostPHID()) { + $post_phid = $this->getPostPHID(); + if (!$post_phid) { + return new Aphront404Response(); + } + + $post = id(new PhamePost())->loadOneWhere( + 'phid = %s', + $post_phid); + + if ($post) { + $this->setPhameTitle($post->getPhameTitle()); + } + + $blogger = id(new PhabricatorUser())->loadOneWhere( + 'phid = %s', $post->getBloggerPHID()); + if (!$blogger) { + return new Aphront404Response(); + } + + } else if ($this->getBloggerName() && $this->getPhameTitle()) { + $phame_title = $this->getPhameTitle(); + $phame_title = PhabricatorSlug::normalize($phame_title); + if ($phame_title != $this->getPhameTitle()) { + $uri = $post->getViewURI($this->getBloggerName()); + return id(new AphrontRedirectResponse())->setURI($uri); + } + $blogger = id(new PhabricatorUser())->loadOneWhere( + 'username = %s', + $this->getBloggerName()); + if (!$blogger) { + return new Aphront404Response(); + } + $post = id(new PhamePost())->loadOneWhere( + 'bloggerPHID = %s AND phameTitle = %s', + $blogger->getPHID(), + $this->getPhameTitle()); + } + + if (!$post) { + return new Aphront404Response(); + } + + if ($post->isDraft() && + $post->getBloggerPHID() != $user->getPHID()) { + return new Aphront404Response(); + } + + if ($post->isDraft()) { + $notice = id(new AphrontErrorView()) + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) + ->setTitle('You are previewing a draft.') + ->setErrors(array( + 'Only you can see this draft until you publish it.', + 'If you chose a comment widget it will show up when you publish.', + )); + } else { + $notice = null; + } + + $page_title = $this->getPhameTitle(); + $page = id(new PhamePostDetailView()) + ->setUser($user) + ->setRequestURI($request->getRequestURI()) + ->setBlogger($blogger) + ->setPost($post); + + $this->setShowSideNav(false); + return $this->buildStandardPageResponse( + array( + $notice, + $page, + ), + array( + 'title' => $page_title, + )); + } +} diff --git a/src/applications/phame/controller/post/view/__init__.php b/src/applications/phame/controller/post/view/__init__.php new file mode 100644 index 0000000000..ad535a74cd --- /dev/null +++ b/src/applications/phame/controller/post/view/__init__.php @@ -0,0 +1,21 @@ +bloggerPHID = $blogger_phid; + return $this; + } + public function withVisibility($visibility) { + $this->visibility = $visibility; + return $this; + } + + public function execute() { + $table = new PhamePost(); + $conn_r = $table->establishConnection('r'); + + $where_clause = $this->buildWhereClause($conn_r); + $order_clause = $this->buildOrderClause($conn_r); + $limit_clause = $this->buildLimitClause($conn_r); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T e %Q %Q %Q', + $table->getTableName(), + $where_clause, + $order_clause, + $limit_clause); + + $posts = $table->loadAllFromArray($data); + + return $posts; + } + + private function buildWhereClause($conn_r) { + $where = array(); + + if ($this->bloggerPHID) { + $where[] = qsprintf( + $conn_r, + 'bloggerPHID = %s', + $this->bloggerPHID + ); + } + + if ($this->visibility !== null) { + $where[] = qsprintf( + $conn_r, + 'visibility = %d', + $this->visibility + ); + } + + return $this->formatWhereClause($where); + } + + private function buildOrderClause($conn_r) { + return 'ORDER BY datePublished DESC, id DESC'; + } +} diff --git a/src/applications/phame/query/post/__init__.php b/src/applications/phame/query/post/__init__.php new file mode 100644 index 0000000000..a1dac94c84 --- /dev/null +++ b/src/applications/phame/query/post/__init__.php @@ -0,0 +1,15 @@ +getPhameTitle()); + $uri = phutil_escape_uri('/phame/posts/'.$blogger_name.'/'.$phame_title); + } else { + $uri = $this->getActionURI('view'); + } + return $uri; + } + public function getEditURI() { + return $this->getActionURI('edit'); + } + public function getDeleteURI() { + return $this->getActionURI('delete'); + } + private function getActionURI($action) { + return '/phame/post/'.$action.'/'.$this->getPHID().'/'; + } + + public function isDraft() { + return $this->getVisibility() == self::VISIBILITY_DRAFT; + } + + public function getCommentsWidget() { + $config_data = $this->getConfigData(); + if (empty($config_data)) { + return 'none'; + } + return idx($config_data, 'comments_widget', 'none'); + } + public function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'configData' => self::SERIALIZATION_JSON, + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorPHIDConstants::PHID_TYPE_POST); + } + + public static function getVisibilityOptionsForSelect() { + return array( + self::VISIBILITY_DRAFT => 'Draft: visible only to me.', + self::VISIBILITY_PUBLISHED => 'Published: visible to the whole world.', + ); + } + + public function getCommentsWidgetOptionsForSelect() { + $current = $this->getCommentsWidget(); + $options = array(); + + if ($current == 'facebook' || + PhabricatorEnv::getEnvConfig('facebook.application-id')) { + $options['facebook'] = 'Facebook'; + } + if ($current == 'disqus' || + PhabricatorEnv::getEnvConfig('disqus.shortname')) { + $options['disqus'] = 'Disqus'; + } + $options['none'] = 'None'; + + return $options; + } + +} diff --git a/src/applications/phame/storage/post/__init__.php b/src/applications/phame/storage/post/__init__.php new file mode 100644 index 0000000000..1806d9c9b4 --- /dev/null +++ b/src/applications/phame/storage/post/__init__.php @@ -0,0 +1,19 @@ +isPreview = $is_preview; + return $this; + } + private function isPreview() { + return $this->isPreview; + } + + public function setUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + public function getUser() { + return $this->user; + } + + public function setRequestURI(PhutilURI $uri) { + $uri = PhabricatorEnv::getProductionURI($uri->setQueryParams(array())); + $this->requestURI = $uri; + return $this; + } + private function getRequestURI() { + return $this->requestURI; + } + + public function setBlogger(PhabricatorUser $blogger) { + $this->blogger = $blogger; + return $this; + } + private function getBlogger() { + return $this->blogger; + } + + public function setPost(PhamePost $post) { + $this->post = $post; + return $this; + } + private function getPost() { + return $this->post; + } + + public function render() { + require_celerity_resource('phabricator-remarkup-css'); + + $user = $this->getUser(); + $blogger = $this->getBlogger(); + $post = $this->getPost(); + $engine = PhabricatorMarkupEngine::newPhameMarkupEngine(); + $body = $engine->markupText($post->getBody()); + if ($post->isDraft()) { + $uri = '/phame/draft/'; + $label = 'Back to Your Drafts'; + } else { + $uri = '/phame/posts/'.$blogger->getUsername(); + $label = 'More Posts by '.phutil_escape_html($blogger->getUsername()); + } + $button = phutil_render_tag( + 'a', + array( + 'href' => $uri, + 'class' => 'grey button', + ), + $label + ); + + $publish_date = $post->getDatePublished(); + if ($publish_date) { + $caption = 'Published '. + phabricator_datetime($publish_date, + $user); + } else { + $caption = 'Last edited '. + phabricator_datetime($post->getDateModified(), + $user); + } + if ($this->isPreview()) { + $width = AphrontPanelView::WIDTH_FULL; + } else { + $width = AphrontPanelView::WIDTH_WIDE; + } + $panel = id(new AphrontPanelView()) + ->setHeader(phutil_escape_html($post->getTitle())) + ->appendChild('
'.$body.'
') + ->setWidth($width) + ->addButton($button) + ->setCaption($caption); + if ($user->getPHID() == $post->getBloggerPHID()) { + if ($post->isDraft()) { + $label = 'Edit Draft'; + } else { + $label = 'Edit Post'; + } + $button = phutil_render_tag( + 'a', + array( + 'href' => $post->getEditURI(), + 'class' => 'grey button', + ), + $label); + $panel->addButton($button); + } + switch ($post->getCommentsWidget()) { + case 'facebook': + $comments = $this->renderFacebookComments(); + break; + case 'disqus': + $comments = $this->renderDisqusComments(); + break; + case 'none': + default: + $comments = null; + break; + } + $panel->appendChild($comments); + + return $panel->render(); + } + + private function renderFacebookComments() { + $fb_id = PhabricatorEnv::getEnvConfig('facebook.application-id'); + if (!$fb_id) { + return null; + } + + $fb_root = phutil_render_tag('div', + array( + 'id' => 'fb-root', + ) + ); + + $c_uri = '//connect.facebook.net/en_US/all.js#xfbml=1&appId='.$fb_id; + $fb_js = jsprintf( + '', + $c_uri + ); + + $fb_comments = phutil_render_tag('div', + array( + 'class' => 'fb-comments', + 'data-href' => $this->getRequestURI(), + 'data-num-posts' => 5, + 'data-width' => 1080, + 'data-colorscheme' => 'dark', + ) + ); + + return '
' . $fb_root . $fb_js . $fb_comments; + } + + private function renderDisqusComments() { + $disqus_shortname = PhabricatorEnv::getEnvConfig('disqus.shortname'); + if (!$disqus_shortname) { + return null; + } + + $post = $this->getPost(); + + $disqus_thread = phutil_render_tag('div', + array( + 'id' => 'disqus_thread' + ) + ); + + // protip - try some var disqus_developer = 1; action to test locally + $disqus_js = jsprintf( + '', + $post->getPHID(), + $this->getRequestURI(), + $post->getTitle() + ); + + return '
' . $disqus_thread . $disqus_js; + } + +} diff --git a/src/applications/phame/view/postdetail/__init__.php b/src/applications/phame/view/postdetail/__init__.php new file mode 100644 index 0000000000..cacc31e370 --- /dev/null +++ b/src/applications/phame/view/postdetail/__init__.php @@ -0,0 +1,21 @@ +draftList = $draft_list; + return $this; + } + public function isDraftList() { + return (bool) $this->draftList; + } + private function getPostNoun() { + if ($this->isDraftList()) { + $noun = 'Draft'; + } else { + $noun = 'Post'; + } + return $noun; + } + + public function setUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + private function getUser() { + return $this->user; + } + public function setPosts(array $posts) { + assert_instances_of($posts, 'PhamePost'); + $this->posts = $posts; + return $this; + } + private function getPosts() { + return $this->posts; + } + public function setBloggers(array $bloggers) { + assert_instances_of($bloggers, 'PhabricatorUser'); + $this->bloggers = $bloggers; + return $this; + } + private function getBloggers() { + return $this->bloggers; + } + public function setActions(array $actions) { + $this->actions = $actions; + return $this; + } + private function getActions() { + if ($this->actions) { + return $this->actions; + } + return array(); + } + + public function render() { + $user = $this->getUser(); + $posts = $this->getPosts(); + $bloggers = $this->getBloggers(); + $noun = $this->getPostNoun(); + + if (empty($posts)) { + $panel = id(new AphrontPanelView()) + ->setHeader(sprintf('No %ss... Yet!', $noun)) + ->setCaption('Will you answer the call to phame?') + ->setCreateButton(sprintf('New %s', $noun), + sprintf('/phame/%s/new', strtolower($noun))); + return $panel->render(); + } + require_celerity_resource('phabricator-remarkup-css'); + + $engine = PhabricatorMarkupEngine::newPhameMarkupEngine(); + $html = array(); + $actions = $this->getActions(); + foreach ($posts as $post) { + $blogger_phid = $post->getBloggerPHID(); + $blogger = $bloggers[$blogger_phid]; + $updated = phabricator_datetime($post->getDateModified(), + $user); + $body = $engine->markupText($post->getBody()); + $panel = id(new AphrontPanelView()) + ->setHeader(phutil_escape_html($post->getTitle())) + ->setCaption('Last updated '.$updated) + ->appendChild('
'.$body.'
'); + foreach ($actions as $action) { + switch ($action) { + case 'view': + $uri = $post->getViewURI($blogger->getUsername()); + $label = 'View '.$noun; + break; + case 'edit': + $uri = $post->getEditURI(); + $label = 'Edit '.$noun; + break; + default: + break; + } + $button = phutil_render_tag( + 'a', + array( + 'href' => $uri, + 'class' => 'grey button', + ), + $label); + $panel->addButton($button); + } + + $html[] = $panel->render(); + } + + return implode('', $html); + } +} diff --git a/src/applications/phame/view/postlist/__init__.php b/src/applications/phame/view/postlist/__init__.php new file mode 100644 index 0000000000..f3ebb90c6b --- /dev/null +++ b/src/applications/phame/view/postlist/__init__.php @@ -0,0 +1,19 @@ +