From ae13d33859138583783379d11f58e32f2d05c695 Mon Sep 17 00:00:00 2001 From: Bob Trahan Date: Thu, 19 Jul 2012 09:03:10 -0700 Subject: [PATCH] Phame - introduce blogs Summary: blogs are collections of posts. a blog also has metadata like a name, description and "bloggers" that can edit the metadata of the blog and contribute posts. changes include the post edit flow where bloggers can now select which blogs to publish to. also made various small tweaks throughout the UI to make things sensical and clean as the concept of blogs is introduced. there's edges powering this stuff. bloggers <=> blogs and posts <=> blogs in particular. Test Plan: made blogs, deleted blogs, tried to make blogs with no bloggers. all went well. verified ui to publish only showed up for public posts, published posts to blogs, un-published posts to blogs, re-published posts to blogs, deleted posts and verified they disappeared from blogs. Reviewers: epriestley Reviewed By: epriestley CC: aran, Korvin Maniphest Tasks: T1373 Differential Revision: https://secure.phabricator.com/D3003 --- resources/sql/patches/phameblog.sql | 31 +++ src/__celerity_resource_map__.php | 63 +++-- src/__phutil_library_map__.php | 24 +- ...AphrontDefaultApplicationConfiguration.php | 10 +- .../phame/controller/PhameController.php | 53 +++- .../blog/PhameBlogDeleteController.php | 115 ++++++++ .../blog/PhameBlogEditController.php | 248 ++++++++++++++++++ .../blog/PhameBlogViewController.php | 170 ++++++++++++ .../blog/list/PhameAllBlogListController.php | 45 ++++ .../blog/list/PhameBlogListBaseController.php | 71 +++++ .../blog/list/PhameUserBlogListController.php | 65 +++++ .../post/PhamePostDeleteController.php | 53 ++-- .../post/PhamePostEditController.php | 188 ++++++++++++- .../post/PhamePostViewController.php | 43 +-- ...ler.php => PhameAllPostListController.php} | 10 +- .../post/list/PhameDraftListController.php | 13 + .../post/list/PhamePostListBaseController.php | 13 +- .../post/list/PhameUserPostListController.php | 37 +-- .../phame/query/PhameBlogQuery.php | 108 ++++++++ .../phame/query/PhamePostQuery.php | 33 ++- src/applications/phame/storage/PhameBlog.php | 135 ++++++++++ src/applications/phame/storage/PhamePost.php | 13 + .../phame/view/PhameBlogDetailView.php | 95 +++++++ .../phame/view/PhameBlogListView.php | 125 +++++++++ .../phid/PhabricatorPHIDConstants.php | 2 +- src/docs/userguide/phame.diviner | 42 ++- .../edges/constants/PhabricatorEdgeConfig.php | 13 +- .../patch/PhabricatorBuiltinPatchList.php | 4 + .../js/application/phame/phame-post-blogs.js | 23 ++ 29 files changed, 1711 insertions(+), 134 deletions(-) create mode 100644 resources/sql/patches/phameblog.sql create mode 100644 src/applications/phame/controller/blog/PhameBlogDeleteController.php create mode 100644 src/applications/phame/controller/blog/PhameBlogEditController.php create mode 100644 src/applications/phame/controller/blog/PhameBlogViewController.php create mode 100644 src/applications/phame/controller/blog/list/PhameAllBlogListController.php create mode 100644 src/applications/phame/controller/blog/list/PhameBlogListBaseController.php create mode 100644 src/applications/phame/controller/blog/list/PhameUserBlogListController.php rename src/applications/phame/controller/post/list/{PhameAllBloggersPostListController.php => PhameAllPostListController.php} (89%) create mode 100644 src/applications/phame/query/PhameBlogQuery.php create mode 100644 src/applications/phame/storage/PhameBlog.php create mode 100644 src/applications/phame/view/PhameBlogDetailView.php create mode 100644 src/applications/phame/view/PhameBlogListView.php create mode 100644 webroot/rsrc/js/application/phame/phame-post-blogs.js diff --git a/resources/sql/patches/phameblog.sql b/resources/sql/patches/phameblog.sql new file mode 100644 index 0000000000..b54d76c8ed --- /dev/null +++ b/resources/sql/patches/phameblog.sql @@ -0,0 +1,31 @@ +CREATE TABLE {$NAMESPACE}_phame.phame_blog ( + `id` INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + `phid` VARCHAR(64) NOT NULL COLLATE utf8_bin, + `name` VARCHAR(64) NOT NULL COLLATE utf8_bin, + `description` LONGTEXT NOT NULL COLLATE utf8_bin, + `configData` LONGTEXT NOT NULL COLLATE utf8_bin, + `creatorPHID` VARCHAR(64) NOT NULL COLLATE utf8_bin, + `dateCreated` INT UNSIGNED NOT NULL, + `dateModified` INT UNSIGNED NOT NULL, + UNIQUE KEY (`phid`) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_phame.edge ( + src VARCHAR(64) NOT NULL COLLATE utf8_bin, + type VARCHAR(64) NOT NULL COLLATE utf8_bin, + dst VARCHAR(64) NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + seq INT UNSIGNED NOT NULL, + dataID INT UNSIGNED, + PRIMARY KEY (src, type, dst), + KEY (src, type, dateCreated, seq) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_phame.edgedata ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + data LONGTEXT NOT NULL COLLATE utf8_bin +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +ALTER TABLE {$NAMESPACE}_phame.phame_post + ADD KEY `instancePosts` (`visibility`, `datePublished`, `id`); + diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 68c82ae38b..35ae5c8dc1 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -1424,6 +1424,17 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/core/behavior-watch-anchor.js', ), + 'javelin-behavior-phame-post-blogs' => + array( + 'uri' => '/res/a7f7756c/rsrc/js/application/phame/phame-post-blogs.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + ), + 'disk' => '/rsrc/js/application/phame/phame-post-blogs.js', + ), 'javelin-behavior-phame-post-preview' => array( 'uri' => '/res/ac4c503a/rsrc/js/application/phame/phame-post-preview.js', @@ -2322,7 +2333,7 @@ celerity_register_resource_map(array( ), 'phabricator-standard-page-view' => array( - 'uri' => '/res/ee3cd0f2/rsrc/css/application/base/standard-page-view.css', + 'uri' => '/res/de559ba2/rsrc/css/application/base/standard-page-view.css', 'type' => 'css', 'requires' => array( @@ -2577,7 +2588,7 @@ celerity_register_resource_map(array( ), array( 'packages' => array( - '3cdc275f' => + '78836d86' => array( 'name' => 'core.pkg.css', 'symbols' => @@ -2606,7 +2617,7 @@ celerity_register_resource_map(array( 21 => 'phabricator-flag-css', 22 => 'aphront-error-view-css', ), - 'uri' => '/res/pkg/3cdc275f/core.pkg.css', + 'uri' => '/res/pkg/78836d86/core.pkg.css', 'type' => 'css', ), 'f363b322' => @@ -2773,20 +2784,20 @@ celerity_register_resource_map(array( 'reverse' => array( 'aphront-attached-file-view-css' => '7839ae2d', - 'aphront-crumbs-view-css' => '3cdc275f', - 'aphront-dialog-view-css' => '3cdc275f', - 'aphront-error-view-css' => '3cdc275f', - 'aphront-form-view-css' => '3cdc275f', + 'aphront-crumbs-view-css' => '78836d86', + 'aphront-dialog-view-css' => '78836d86', + 'aphront-error-view-css' => '78836d86', + 'aphront-form-view-css' => '78836d86', 'aphront-headsup-action-list-view-css' => '96bc37d6', - 'aphront-headsup-view-css' => '3cdc275f', - 'aphront-list-filter-view-css' => '3cdc275f', - 'aphront-pager-view-css' => '3cdc275f', - 'aphront-panel-view-css' => '3cdc275f', - 'aphront-side-nav-view-css' => '3cdc275f', - 'aphront-table-view-css' => '3cdc275f', - 'aphront-tokenizer-control-css' => '3cdc275f', - 'aphront-tooltip-css' => '3cdc275f', - 'aphront-typeahead-control-css' => '3cdc275f', + 'aphront-headsup-view-css' => '78836d86', + 'aphront-list-filter-view-css' => '78836d86', + 'aphront-pager-view-css' => '78836d86', + 'aphront-panel-view-css' => '78836d86', + 'aphront-side-nav-view-css' => '78836d86', + 'aphront-table-view-css' => '78836d86', + 'aphront-tokenizer-control-css' => '78836d86', + 'aphront-tooltip-css' => '78836d86', + 'aphront-typeahead-control-css' => '78836d86', 'differential-changeset-view-css' => '96bc37d6', 'differential-core-view-css' => '96bc37d6', 'differential-inline-comment-editor' => 'f4bbbd84', @@ -2852,15 +2863,15 @@ celerity_register_resource_map(array( 'javelin-workflow' => 'f363b322', 'maniphest-task-summary-css' => '7839ae2d', 'maniphest-transaction-detail-css' => '7839ae2d', - 'phabricator-app-buttons-css' => '3cdc275f', + 'phabricator-app-buttons-css' => '78836d86', 'phabricator-content-source-view-css' => '96bc37d6', - 'phabricator-core-buttons-css' => '3cdc275f', - 'phabricator-core-css' => '3cdc275f', - 'phabricator-directory-css' => '3cdc275f', + 'phabricator-core-buttons-css' => '78836d86', + 'phabricator-core-css' => '78836d86', + 'phabricator-directory-css' => '78836d86', 'phabricator-drag-and-drop-file-upload' => 'f4bbbd84', 'phabricator-dropdown-menu' => 'f363b322', - 'phabricator-flag-css' => '3cdc275f', - 'phabricator-jump-nav' => '3cdc275f', + 'phabricator-flag-css' => '78836d86', + 'phabricator-jump-nav' => '78836d86', 'phabricator-keyboard-shortcut' => 'f363b322', 'phabricator-keyboard-shortcut-manager' => 'f363b322', 'phabricator-menu-item' => 'f363b322', @@ -2868,11 +2879,11 @@ celerity_register_resource_map(array( 'phabricator-paste-file-upload' => 'f363b322', 'phabricator-prefab' => 'f363b322', 'phabricator-project-tag-css' => '7839ae2d', - 'phabricator-remarkup-css' => '3cdc275f', + 'phabricator-remarkup-css' => '78836d86', 'phabricator-shaped-request' => 'f4bbbd84', - 'phabricator-standard-page-view' => '3cdc275f', + 'phabricator-standard-page-view' => '78836d86', 'phabricator-tooltip' => 'f363b322', - 'phabricator-transaction-view-css' => '3cdc275f', - 'syntax-highlighting-css' => '3cdc275f', + 'phabricator-transaction-view-css' => '78836d86', + 'syntax-highlighting-css' => '78836d86', ), )); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f36d58484b..32c72cd619 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1051,7 +1051,16 @@ phutil_register_library_map(array( 'PhabricatorXHProfProfileSymbolView' => 'applications/xhprof/view/PhabricatorXHProfProfileSymbolView.php', 'PhabricatorXHProfProfileTopLevelView' => 'applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php', 'PhabricatorXHProfProfileView' => 'applications/xhprof/view/PhabricatorXHProfProfileView.php', - 'PhameAllBloggersPostListController' => 'applications/phame/controller/post/list/PhameAllBloggersPostListController.php', + 'PhameAllBlogListController' => 'applications/phame/controller/blog/list/PhameAllBlogListController.php', + 'PhameAllPostListController' => 'applications/phame/controller/post/list/PhameAllPostListController.php', + 'PhameBlog' => 'applications/phame/storage/PhameBlog.php', + 'PhameBlogDeleteController' => 'applications/phame/controller/blog/PhameBlogDeleteController.php', + 'PhameBlogDetailView' => 'applications/phame/view/PhameBlogDetailView.php', + 'PhameBlogEditController' => 'applications/phame/controller/blog/PhameBlogEditController.php', + 'PhameBlogListBaseController' => 'applications/phame/controller/blog/list/PhameBlogListBaseController.php', + 'PhameBlogListView' => 'applications/phame/view/PhameBlogListView.php', + 'PhameBlogQuery' => 'applications/phame/query/PhameBlogQuery.php', + 'PhameBlogViewController' => 'applications/phame/controller/blog/PhameBlogViewController.php', 'PhameBloggerPostListController' => 'applications/phame/controller/post/list/PhameBloggerPostListController.php', 'PhameController' => 'applications/phame/controller/PhameController.php', 'PhameDAO' => 'applications/phame/storage/PhameDAO.php', @@ -1065,6 +1074,7 @@ phutil_register_library_map(array( 'PhamePostPreviewController' => 'applications/phame/controller/post/PhamePostPreviewController.php', 'PhamePostQuery' => 'applications/phame/query/PhamePostQuery.php', 'PhamePostViewController' => 'applications/phame/controller/post/PhamePostViewController.php', + 'PhameUserBlogListController' => 'applications/phame/controller/blog/list/PhameUserBlogListController.php', 'PhameUserPostListController' => 'applications/phame/controller/post/list/PhameUserPostListController.php', 'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php', 'PhortuneStripeBaseController' => 'applications/phortune/stripe/controller/PhortuneStripeBaseController.php', @@ -2031,7 +2041,16 @@ phutil_register_library_map(array( 'PhabricatorXHProfProfileSymbolView' => 'PhabricatorXHProfProfileView', 'PhabricatorXHProfProfileTopLevelView' => 'PhabricatorXHProfProfileView', 'PhabricatorXHProfProfileView' => 'AphrontView', - 'PhameAllBloggersPostListController' => 'PhamePostListBaseController', + 'PhameAllBlogListController' => 'PhameBlogListBaseController', + 'PhameAllPostListController' => 'PhamePostListBaseController', + 'PhameBlog' => 'PhameDAO', + 'PhameBlogDeleteController' => 'PhameController', + 'PhameBlogDetailView' => 'AphrontView', + 'PhameBlogEditController' => 'PhameController', + 'PhameBlogListBaseController' => 'PhameController', + 'PhameBlogListView' => 'AphrontView', + 'PhameBlogQuery' => 'PhabricatorOffsetPagedQuery', + 'PhameBlogViewController' => 'PhameController', 'PhameBloggerPostListController' => 'PhamePostListBaseController', 'PhameController' => 'PhabricatorController', 'PhameDAO' => 'PhabricatorLiskDAO', @@ -2045,6 +2064,7 @@ phutil_register_library_map(array( 'PhamePostPreviewController' => 'PhameController', 'PhamePostQuery' => 'PhabricatorOffsetPagedQuery', 'PhamePostViewController' => 'PhameController', + 'PhameUserBlogListController' => 'PhameBlogListBaseController', 'PhameUserPostListController' => 'PhamePostListBaseController', 'PhortuneMonthYearExpiryControl' => 'AphrontFormControl', 'PhortuneStripeBaseController' => 'PhabricatorController', diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php index 54a53bad35..8b0b348000 100644 --- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php @@ -382,7 +382,7 @@ class AphrontDefaultApplicationConfiguration ), '/phame/' => array( - '' => 'PhameAllBloggersPostListController', + '' => 'PhameAllPostListController', 'post/' => array( '' => 'PhameUserPostListController', 'delete/(?P[^/]+)/' => 'PhamePostDeleteController', @@ -395,6 +395,14 @@ class AphrontDefaultApplicationConfiguration '' => 'PhameDraftListController', 'new/' => 'PhamePostEditController', ), + 'blog/' => array( + '' => 'PhameUserBlogListController', + 'all/' => 'PhameAllBlogListController', + 'new/' => 'PhameBlogEditController', + 'delete/(?P[^/]+)/' => 'PhameBlogDeleteController', + 'edit/(?P[^/]+)/' => 'PhameBlogEditController', + 'view/(?P[^/]+)/' => 'PhameBlogViewController', + ), 'posts/' => array( '' => 'PhameUserPostListController', '(?P\w+)/' => 'PhameBloggerPostListController', diff --git a/src/applications/phame/controller/PhameController.php b/src/applications/phame/controller/PhameController.php index 2f5c451667..af1ec4ec17 100644 --- a/src/applications/phame/controller/PhameController.php +++ b/src/applications/phame/controller/PhameController.php @@ -68,12 +68,18 @@ abstract class PhameController extends PhabricatorController { 'New Draft'); $nav->addFilter('draft', 'My Drafts'); + foreach ($this->getSideNavExtraDraftFilters() as $draft_filter) { + $nav->addFilter($draft_filter['key'], + $draft_filter['name'], + idx($draft_filter, 'uri')); + } + $nav->addSpacer(); $nav->addLabel('Posts'); $nav->addFilter('post', 'My Posts'); - $nav->addFilter('everyone', - 'Everyone', + $nav->addFilter('post/all', + 'All Posts', $base_uri); foreach ($this->getSideNavExtraPostFilters() as $post_filter) { $nav->addFilter($post_filter['key'], @@ -81,16 +87,59 @@ abstract class PhameController extends PhabricatorController { idx($post_filter, 'uri')); } + $nav->addSpacer(); + $nav->addLabel('Blogs'); + foreach ($this->getSideNavBlogFilters() as $blog_filter) { + $nav->addFilter($blog_filter['key'], + $blog_filter['name'], + idx($blog_filter, 'uri')); + } + $nav->selectFilter($filter); return $nav; } + protected function getSideNavExtraDraftFilters() { + return array(); + } + protected function getSideNavExtraPostFilters() { return array(); } + + protected function getSideNavBlogFilters() { + return array( + array( + 'key' => 'blog', + 'name' => 'My Blogs', + ), + array( + 'key' => 'blog/all', + 'name' => 'All Blogs', + ), + ); + } + protected function getSideNavFilter() { return 'post'; } + protected function getPager() { + $request = $this->getRequest(); + $pager = new AphrontPagerView(); + $page_size = 50; + $pager->setURI($request->getRequestURI(), 'offset'); + $pager->setPageSize($page_size); + $pager->setOffset($request->getInt('offset')); + + return $pager; + } + + protected function buildNoticeView() { + $notice_view = id(new AphrontErrorView()) + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) + ->setTitle('Meta thoughts and feelings'); + return $notice_view; + } } diff --git a/src/applications/phame/controller/blog/PhameBlogDeleteController.php b/src/applications/phame/controller/blog/PhameBlogDeleteController.php new file mode 100644 index 0000000000..0eb2ef6512 --- /dev/null +++ b/src/applications/phame/controller/blog/PhameBlogDeleteController.php @@ -0,0 +1,115 @@ +phid = $phid; + return $this; + } + private function getBlogPHID() { + return $this->phid; + } + + protected function getSideNavFilter() { + return 'blog/delete/'.$this->getBlogPHID(); + } + + protected function getSideNavExtraBlogFilters() { + $filters = array( + array('key' => $this->getSideNavFilter(), + 'name' => 'Delete Blog') + ); + + return $filters; + } + + public function willProcessRequest(array $data) { + $phid = $data['phid']; + $this->setBlogPHID($phid); + } + + public function processRequest() { + $blogger_edge_type = PhabricatorEdgeConfig::TYPE_BLOG_HAS_BLOGGER; + $post_edge_type = PhabricatorEdgeConfig::TYPE_BLOG_HAS_POST; + $request = $this->getRequest(); + $user = $request->getUser(); + $blog_phid = $this->getBlogPHID(); + $blogs = id(new PhameBlogQuery()) + ->withPHIDs(array($blog_phid)) + ->execute(); + $blog = reset($blogs); + if (empty($blog)) { + return new Aphront404Response(); + } + + $phids = array($blog_phid); + $edge_types = array( + PhabricatorEdgeConfig::TYPE_BLOG_HAS_BLOGGER, + PhabricatorEdgeConfig::TYPE_BLOG_HAS_POST, + ); + + $edges = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($phids) + ->withEdgeTypes($edge_types) + ->execute(); + + $blogger_edges = $edges[$blog_phid][$blogger_edge_type]; + // TODO -- make this check use a policy + if (!isset($blogger_edges[$user->getPHID()]) && + !$user->isAdmin()) { + return new Aphront403Response(); + } + + $edit_uri = $blog->getEditURI(); + + if ($request->isFormPost()) { + $blogger_phids = array_keys($blogger_edges); + $post_edges = $edges[$blog_phid][$post_edge_type]; + $post_phids = array_keys($post_edges); + $editor = id(new PhabricatorEdgeEditor()); + $editor->setUser($user); + foreach ($blogger_phids as $phid) { + $editor->removeEdge($blog_phid, $blogger_edge_type, $phid); + } + foreach ($post_phids as $phid) { + $editor->removeEdge($blog_phid, $post_edge_type, $phid); + } + $editor->save(); + + $blog->delete(); + return id(new AphrontRedirectResponse()) + ->setURI('/phame/blog/?deleted'); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($user) + ->setTitle('Delete blog?') + ->appendChild('Really delete this blog? It will be gone forever.') + ->addSubmitButton('Delete') + ->addCancelButton($edit_uri); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } +} diff --git a/src/applications/phame/controller/blog/PhameBlogEditController.php b/src/applications/phame/controller/blog/PhameBlogEditController.php new file mode 100644 index 0000000000..352c8e780f --- /dev/null +++ b/src/applications/phame/controller/blog/PhameBlogEditController.php @@ -0,0 +1,248 @@ +phid = $phid; + return $this; + } + private function getBlogPHID() { + return $this->phid; + } + private function setIsBlogEdit($is_blog_edit) { + $this->isBlogEdit = $is_blog_edit; + return $this; + } + private function isBlogEdit() { + return $this->isBlogEdit; + } + + protected function getSideNavFilter() { + if ($this->isBlogEdit()) { + $filter = 'blog/edit/'.$this->getBlogPHID(); + } else { + $filter = 'blog/new'; + } + return $filter; + } + + protected function getSideNavBlogFilters() { + $filters = parent::getSideNavBlogFilters(); + + if ($this->isBlogEdit()) { + $filter = + array('key' => 'blog/edit/'.$this->getBlogPHID(), + 'name' => 'Edit Blog'); + $filters[] = $filter; + } else { + $filter = + array('key' => 'blog/new', + 'name' => 'New Blog'); + array_unshift($filters, $filter); + } + + return $filters; + } + + public function willProcessRequest(array $data) { + $phid = idx($data, 'phid'); + $this->setBlogPHID($phid); + $this->setIsBlogEdit((bool)$phid); + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + $e_name = null; + $e_bloggers = null; + $errors = array(); + + if ($this->isBlogEdit()) { + $blogs = id(new PhameBlogQuery()) + ->withPHIDs(array($this->getBlogPHID())) + ->execute(); + $blog = reset($blogs); + if (empty($blog)) { + return new Aphront404Response(); + } + + $bloggers = $blog->loadBloggers()->getBloggers(); + + // TODO -- make this check use a policy + if (!isset($bloggers[$user->getPHID()]) && + !$user->isAdmin()) { + return new Aphront403Response(); + } + $blogger_tokens = mpull($bloggers, 'getFullName', 'getPHID'); + $submit_button = 'Save Changes'; + $delete_button = javelin_render_tag( + 'a', + array( + 'href' => $blog->getDeleteURI(), + 'class' => 'grey button', + 'sigil' => 'workflow', + ), + 'Delete Blog'); + $page_title = 'Edit Blog'; + } else { + $blog = id(new PhameBlog()) + ->setCreatorPHID($user->getPHID()); + $blogger_tokens = array($user->getPHID() => $user->getFullName()); + $submit_button = 'Create Blog'; + $delete_button = null; + $page_title = 'Create Blog'; + } + + if ($request->isFormPost()) { + $saved = true; + $name = $request->getStr('name'); + $description = $request->getStr('description'); + $blogger_arr = $request->getArr('bloggers'); + + if (empty($blogger_arr)) { + $error = 'Bloggers must be nonempty.'; + if ($this->isBlogEdit()) { + $error .= ' To delete the blog, use the delete button.'; + } else { + $error .= ' A blog cannot exist without bloggers.'; + } + $e_bloggers = 'Required'; + $errors[] = $error; + } + $new_bloggers = array_values($blogger_arr); + if ($this->isBlogEdit()) { + $old_bloggers = array_keys($blogger_tokens); + } else { + $old_bloggers = array(); + } + + if (empty($name)) { + $errors[] = 'Name must be nonempty.'; + $e_name = 'Required'; + } + $blog->setName($name); + $blog->setDescription($description); + + if (empty($errors)) { + $blog->save(); + + $add_phids = $new_bloggers; + $rem_phids = array_diff($old_bloggers, $new_bloggers); + $editor = new PhabricatorEdgeEditor(); + $edge_type = PhabricatorEdgeConfig::TYPE_BLOG_HAS_BLOGGER; + $editor->setUser($user); + foreach ($add_phids as $phid) { + $editor->addEdge($blog->getPHID(), $edge_type, $phid); + } + foreach ($rem_phids as $phid) { + $editor->removeEdge($blog->getPHID(), $edge_type, $phid); + } + $editor->save(); + + } else { + $saved = false; + } + + if ($saved) { + $uri = new PhutilURI($blog->getViewURI()); + $uri->setQueryParam('new', true); + 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('Name') + ->setName('name') + ->setValue($blog->getName()) + ->setID('blog-name') + ->setError($e_name) + ) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel('Description') + ->setName('description') + ->setValue($blog->getDescription()) + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) + ->setID('blog-description') + ->setCaption($remarkup_reference) + ) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setLabel('Bloggers') + ->setName('bloggers') + ->setValue($blogger_tokens) + ->setUser($user) + ->setDatasource('/typeahead/common/users/') + ->setError($e_bloggers) + ) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton('/phame/blog/') + ->setValue($submit_button) + ); + + $panel->appendChild($form); + + if ($errors) { + $error_view = id(new AphrontErrorView()) + ->setTitle('Errors saving blog.') + ->setErrors($errors); + } else { + $error_view = null; + } + + $this->setShowSideNav(true); + return $this->buildStandardPageResponse( + array( + $error_view, + $panel, + ), + array( + 'title' => $page_title, + )); + } +} diff --git a/src/applications/phame/controller/blog/PhameBlogViewController.php b/src/applications/phame/controller/blog/PhameBlogViewController.php new file mode 100644 index 0000000000..96af259859 --- /dev/null +++ b/src/applications/phame/controller/blog/PhameBlogViewController.php @@ -0,0 +1,170 @@ +postPHIDs = $post_phids; + return $this; + } + private function getPostPHIDs() { + return $this->postPHIDs; + } + + private function setBloggerPHIDs($blogger_phids) { + $this->bloggerPHIDs = $blogger_phids; + return $this; + } + private function getBloggerPHIDs() { + return $this->bloggerPHIDs; + } + + private function setBlogPHID($blog_phid) { + $this->blogPHID = $blog_phid; + return $this; + } + private function getBlogPHID() { + return $this->blogPHID; + } + + protected function getSideNavFilter() { + $filter = 'blog/view/'.$this->getBlogPHID(); + return $filter; + } + protected function getSideNavExtraBlogFilters() { + $filters = array( + array('key' => $this->getSideNavFilter(), + 'name' => $this->getPhameTitle()) + ); + return $filters; + } + + public function willProcessRequest(array $data) { + $this->setBlogPHID(idx($data, 'phid')); + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + $blog_phid = $this->getBlogPHID(); + + $blogs = id(new PhameBlogQuery()) + ->withPHIDs(array($blog_phid)) + ->execute(); + $blog = reset($blogs); + + if (!$blog) { + return new Aphront404Response(); + } + + $this->loadEdges(); + + $blogger_phids = $this->getBloggerPHIDs(); + if ($blogger_phids) { + $bloggers = id(new PhabricatorObjectHandleData($blogger_phids)) + ->loadHandles(); + } else { + $bloggers = array(); + } + + $post_phids = $this->getPostPHIDs(); + if ($post_phids) { + $posts = id(new PhamePostQuery()) + ->withPHIDs($post_phids) + ->withVisibility(PhamePost::VISIBILITY_PUBLISHED) + ->execute(); + } else { + $posts = array(); + } + + $actions = array('view'); + $is_admin = false; + // TODO -- make this check use a policy + if (isset($bloggers[$user->getPHID()])) { + $actions[] = 'edit'; + $is_admin = true; + } + + if ($request->getExists('new')) { + $notice = $this->buildNoticeView() + ->setTitle('Successfully created your blog.') + ->appendChild('Time to write some posts.'); + } else { + $notice = null; + } + + $details = id(new PhameBlogDetailView()) + ->setUser($user) + ->setBloggers($bloggers) + ->setBlog($blog) + ->setIsAdmin($is_admin); + + $panel = id(new PhamePostListView()) + ->setUser($this->getRequest()->getUser()) + ->setBloggers($bloggers) + ->setPosts($posts) + ->setActions($actions) + ->setDraftList(false); + + $this->setShowSideNav(false); + + return $this->buildStandardPageResponse( + array( + $notice, + $details, + $panel, + ), + array( + 'title' => $blog->getName(), + )); + } + + private function loadEdges() { + + $edge_types = array( + PhabricatorEdgeConfig::TYPE_BLOG_HAS_BLOGGER, + PhabricatorEdgeConfig::TYPE_BLOG_HAS_POST, + ); + $blog_phid = $this->getBlogPHID(); + $phids = array($blog_phid); + + $edges = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($phids) + ->withEdgeTypes($edge_types) + ->execute(); + + $blogger_phids = array_keys( + $edges[$blog_phid][PhabricatorEdgeConfig::TYPE_BLOG_HAS_BLOGGER] + ); + $this->setBloggerPHIDs($blogger_phids); + + $post_phids = array_keys( + $edges[$blog_phid][PhabricatorEdgeConfig::TYPE_BLOG_HAS_POST] + ); + $this->setPostPHIDs($post_phids); + + } +} diff --git a/src/applications/phame/controller/blog/list/PhameAllBlogListController.php b/src/applications/phame/controller/blog/list/PhameAllBlogListController.php new file mode 100644 index 0000000000..f17c58830d --- /dev/null +++ b/src/applications/phame/controller/blog/list/PhameAllBlogListController.php @@ -0,0 +1,45 @@ +getRequest()->getUser(); + + $blogs = id(new PhameBlogQuery()) + ->needBloggers(true) + ->executeWithPager($this->getPager()); + $this->setBlogs($blogs); + + $page_title = 'All Blogs'; + $this->setPageTitle($page_title); + + $this->setShowSideNav(true); + + return $this->buildBlogListPageResponse(); + } + +} diff --git a/src/applications/phame/controller/blog/list/PhameBlogListBaseController.php b/src/applications/phame/controller/blog/list/PhameBlogListBaseController.php new file mode 100644 index 0000000000..5c509410a3 --- /dev/null +++ b/src/applications/phame/controller/blog/list/PhameBlogListBaseController.php @@ -0,0 +1,71 @@ +blogs = $blogs; + return $this; + } + private function getBlogs() { + return $this->blogs; + } + + protected function setPageTitle($page_title) { + $this->pageTitle = $page_title; + return $this; + } + private function getPageTitle() { + return $this->pageTitle; + } + + protected function getNoticeView() { + return null; + } + + private function getBlogListPanel() { + $blogs = $this->getBlogs(); + + $panel = id(new PhameBlogListView()) + ->setUser($this->getRequest()->getUser()) + ->setBlogs($blogs) + ->setHeader($this->getPageTitle()); + + return $panel; + } + + protected function buildBlogListPageResponse() { + return $this->buildStandardPageResponse( + array( + $this->getNoticeView(), + $this->getBlogListPanel(), + $this->getPager(), + ), + array( + 'title' => $this->getPageTitle(), + )); + } +} diff --git a/src/applications/phame/controller/blog/list/PhameUserBlogListController.php b/src/applications/phame/controller/blog/list/PhameUserBlogListController.php new file mode 100644 index 0000000000..acbb9f1ad4 --- /dev/null +++ b/src/applications/phame/controller/blog/list/PhameUserBlogListController.php @@ -0,0 +1,65 @@ +getRequest(); + + if ($request->getExists('deleted')) { + $notice_view = $this->buildNoticeView() + ->appendChild('Successfully deleted blog.'); + } else { + $notice_view = null; + } + + return $notice_view; + } + + protected function getSideNavFilter() { + return 'blog'; + } + + public function processRequest() { + $user = $this->getRequest()->getUser(); + $phid = $user->getPHID(); + + $blog_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $phid, + PhabricatorEdgeConfig::TYPE_BLOGGER_HAS_BLOG + ); + + $blogs = id(new PhameBlogQuery()) + ->withPHIDs($blog_phids) + ->needBloggers(true) + ->executeWithPager($this->getPager()); + + $this->setBlogs($blogs); + + $this->setPageTitle('My Blogs'); + + $this->setShowSideNav(true); + + return $this->buildBlogListPageResponse(); + } + +} diff --git a/src/applications/phame/controller/post/PhamePostDeleteController.php b/src/applications/phame/controller/post/PhamePostDeleteController.php index b4fa6de994..ed94af410c 100644 --- a/src/applications/phame/controller/post/PhamePostDeleteController.php +++ b/src/applications/phame/controller/post/PhamePostDeleteController.php @@ -32,47 +32,54 @@ extends PhameController { 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()); + $request = $this->getRequest(); + $user = $request->getUser(); + $post_phid = $this->getPostPHID(); + $posts = id(new PhamePostQuery()) + ->withPHIDs(array($post_phid)) + ->execute(); + $post = reset($posts); if (empty($post)) { return new Aphront404Response(); } if ($post->getBloggerPHID() != $user->getPHID()) { return new Aphront403Response(); } - $edit_uri = $post->getEditURI(); + $post_noun = $post->getHumanName(); if ($request->isFormPost()) { + $edge_type = PhabricatorEdgeConfig::TYPE_POST_HAS_BLOG; + $edges = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(array($post_phid)) + ->withEdgeTypes(array($edge_type)) + ->execute(); + + $blog_edges = $edges[$post_phid][$edge_type]; + $blog_phids = array_keys($blog_edges); + $editor = id(new PhabricatorEdgeEditor()); + $editor->setUser($user); + foreach ($blog_phids as $phid) { + $editor->removeEdge($post_phid, $edge_type, $phid); + } + $editor->save(); + $post->delete(); - return id(new AphrontRedirectResponse())->setURI('/phame/?deleted'); + return id(new AphrontRedirectResponse()) + ->setURI('/phame/'.$post_noun.'/?deleted'); } - $dialog = id(new AphrontDialogView()) + $edit_uri = $post->getEditURI(); + $dialog = id(new AphrontDialogView()) ->setUser($user) - ->setTitle('Delete post?') - ->appendChild('Really delete this post? It will be gone forever.') + ->setTitle('Delete '.$post_noun.'?') + ->appendChild('Really delete this '.$post_noun.'? '. + 'It will be gone forever.') ->addSubmitButton('Delete') ->addCancelButton($edit_uri); diff --git a/src/applications/phame/controller/post/PhamePostEditController.php b/src/applications/phame/controller/post/PhamePostEditController.php index 3d1c005c96..732d7c147b 100644 --- a/src/applications/phame/controller/post/PhamePostEditController.php +++ b/src/applications/phame/controller/post/PhamePostEditController.php @@ -20,10 +20,21 @@ * @group phame */ final class PhamePostEditController -extends PhameController { + extends PhameController { private $phid; private $isPostEdit; + private $userBlogs; + private $postBlogs; + private $post; + + private function setPost(PhamePost $post) { + $this->post = $post; + return $this; + } + private function getPost() { + return $this->post; + } private function setPostPHID($phid) { $this->phid = $phid; @@ -39,17 +50,34 @@ extends PhameController { private function isPostEdit() { return $this->isPostEdit; } + private function setUserBlogs(array $blogs) { + assert_instances_of($blogs, 'PhameBlog'); + $this->userBlogs = $blogs; + return $this; + } + private function getUserBlogs() { + return $this->userBlogs; + } + private function setPostBlogs(array $blogs) { + assert_instances_of($blogs, 'PhameBlog'); + $this->postBlogs = $blogs; + return $this; + } + private function getPostBlogs() { + return $this->postBlogs; + } protected function getSideNavFilter() { if ($this->isPostEdit()) { - $filter = 'post/edit/'.$this->getPostPHID(); + $post_noun = $this->getPost()->getHumanName(); + $filter = $post_noun.'/edit/'.$this->getPostPHID(); } else { $filter = 'post/new'; } return $filter; } protected function getSideNavExtraPostFilters() { - if ($this->isPostEdit()) { + if ($this->isPostEdit() && !$this->getPost()->isDraft()) { $filters = array( array('key' => 'post/edit/'.$this->getPostPHID(), 'name' => 'Edit Post') @@ -60,6 +88,18 @@ extends PhameController { return $filters; } + protected function getSideNavExtraDraftFilters() { + if ($this->isPostEdit() && $this->getPost()->isDraft()) { + $filters = array( + array('key' => 'draft/edit/'.$this->getPostPHID(), + 'name' => 'Edit Draft') + ); + } else { + $filters = array(); + } + + return $filters; + } public function willProcessRequest(array $data) { $phid = idx($data, 'phid'); @@ -75,15 +115,17 @@ extends PhameController { $errors = array(); if ($this->isPostEdit()) { - $post = id(new PhamePost())->loadOneWhere( - 'phid = %s', - $this->getPostPHID()); + $posts = id(new PhamePostQuery()) + ->withPHIDs(array($this->getPostPHID())) + ->execute(); + $post = reset($posts); if (empty($post)) { return new Aphront404Response(); } if ($post->getBloggerPHID() != $user->getPHID()) { return new Aphront403Response(); } + $post_noun = ucfirst($post->getHumanName()); $cancel_uri = $post->getViewURI($user->getUsername()); $submit_button = 'Save Changes'; $delete_button = javelin_render_tag( @@ -93,17 +135,19 @@ extends PhameController { 'class' => 'grey button', 'sigil' => 'workflow', ), - 'Delete Post'); - $page_title = 'Edit Post'; + 'Delete '.$post_noun); + $page_title = 'Edit '.$post_noun; } else { $post = id(new PhamePost()) ->setBloggerPHID($user->getPHID()) ->setVisibility(PhamePost::VISIBILITY_DRAFT); - $cancel_uri = '/phame/'; - $submit_button = 'Create Post'; + $cancel_uri = '/phame/draft/'; + $submit_button = 'Create Draft'; $delete_button = null; - $page_title = 'Create Post'; + $page_title = 'Create Draft'; } + $this->setPost($post); + $this->loadEdgesAndBlogs(); if ($request->isFormPost()) { $saved = true; @@ -135,9 +179,34 @@ extends PhameController { $errors[] = 'Title must be nonempty.'; $e_title = 'Required'; } + + $blogs_published = array_keys($this->getPostBlogs()); + $blogs_to_publish = array(); + $blogs_to_depublish = array(); + if ($visibility == PhamePost::VISIBILITY_PUBLISHED) { + $blogs_arr = $request->getArr('blogs'); + $blogs_to_publish = array_values($blogs_arr); + $blogs_to_depublish = array_diff($blogs_published, + $blogs_to_publish); + } else { + $blogs_to_depublish = $blogs_published; + } + if (empty($errors)) { try { $post->save(); + + $editor = new PhabricatorEdgeEditor(); + $edge_type = PhabricatorEdgeConfig::TYPE_POST_HAS_BLOG; + $editor->setUser($user); + foreach ($blogs_to_publish as $phid) { + $editor->addEdge($post->getPHID(), $edge_type, $phid); + } + foreach ($blogs_to_depublish as $phid) { + $editor->removeEdge($post->getPHID(), $edge_type, $phid); + } + $editor->save(); + } catch (AphrontQueryDuplicateKeyException $e) { $saved = false; $e_phame_title = 'Not Unique'; @@ -150,6 +219,7 @@ extends PhameController { if ($saved) { $uri = new PhutilURI($post->getViewURI($user->getUsername())); + $uri->setQueryParam('saved', true); return id(new AphrontRedirectResponse()) ->setURI($uri); } @@ -209,6 +279,10 @@ extends PhameController { ->setName('visibility') ->setValue($post->getVisibility()) ->setOptions(PhamePost::getVisibilityOptionsForSelect()) + ->setID('post-visibility') + ) + ->appendChild( + $this->getBlogCheckboxControl($post) ) ->appendChild( id(new AphrontFormSelectControl()) @@ -247,6 +321,26 @@ extends PhameController { 'uri' => '/phame/post/preview/', )); + $visibility_data = array( + 'select_id' => 'post-visibility', + 'current' => $post->getVisibility(), + 'published' => PhamePost::VISIBILITY_PUBLISHED, + 'draft' => PhamePost::VISIBILITY_DRAFT, + 'change_uri' => $post->getChangeVisibilityURI(), + ); + + $blogs_data = array( + 'checkbox_id' => 'post-blogs', + 'have_published' => (bool) count($this->getPostBlogs()) + ); + + Javelin::initBehavior( + 'phame-post-blogs', + array( + 'blogs' => $blogs_data, + 'visibility' => $visibility_data, + )); + if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle('Errors saving post.') @@ -266,4 +360,76 @@ extends PhameController { 'title' => $page_title, )); } + + private function getBlogCheckboxControl(PhamePost $post) { + if ($post->getVisibility() == PhamePost::VISIBILITY_PUBLISHED) { + $control_style = null; + } else { + $control_style = 'display: none'; + } + + $control = id(new AphrontFormCheckboxControl()) + ->setLabel('Blogs') + ->setControlID('post-blogs') + ->setControlStyle($control_style); + + $post_blogs = $this->getPostBlogs(); + $user_blogs = $this->getUserBlogs(); + $all_blogs = $post_blogs + $user_blogs; + $all_blogs = msort($all_blogs, 'getName'); + foreach ($all_blogs as $phid => $blog) { + $control->addCheckbox( + 'blogs[]', + $blog->getPHID(), + $blog->getName(), + isset($post_blogs[$phid]) + ); + } + + return $control; + } + + private function loadEdgesAndBlogs() { + $edge_types = array(PhabricatorEdgeConfig::TYPE_BLOGGER_HAS_BLOG); + $blogger_phid = $this->getRequest()->getUser()->getPHID(); + $phids = array($blogger_phid); + if ($this->isPostEdit()) { + $edge_types[] = PhabricatorEdgeConfig::TYPE_POST_HAS_BLOG; + $phids[] = $this->getPostPHID(); + } + + $edges = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($phids) + ->withEdgeTypes($edge_types) + ->execute(); + + $all_blogs_assoc = array(); + foreach ($phids as $phid) { + foreach ($edge_types as $type) { + $all_blogs_assoc += $edges[$phid][$type]; + } + } + + $blogs = id(new PhameBlogQuery()) + ->withPHIDs(array_keys($all_blogs_assoc)) + ->execute(); + $blogs = mpull($blogs, null, 'getPHID'); + + $user_blogs = array_intersect_key( + $blogs, + $edges[$blogger_phid][PhabricatorEdgeConfig::TYPE_BLOGGER_HAS_BLOG] + ); + + if ($this->isPostEdit()) { + $post_blogs = array_intersect_key( + $blogs, + $edges[$this->getPostPHID()][PhabricatorEdgeConfig::TYPE_POST_HAS_BLOG] + ); + } else { + $post_blogs = array(); + } + + $this->setUserBlogs($user_blogs); + $this->setPostBlogs($post_blogs); + } } diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php index 9122ea682e..b1123a1d44 100644 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -83,18 +83,18 @@ extends PhameController { return new Aphront404Response(); } - $post = id(new PhamePost())->loadOneWhere( - 'phid = %s', - $post_phid); + $posts = id(new PhamePostQuery()) + ->withPHIDs(array($post_phid)) + ->execute(); + $post = reset($posts); if ($post) { $this->setPhameTitle($post->getPhameTitle()); - } - - $blogger = id(new PhabricatorUser())->loadOneWhere( - 'phid = %s', $post->getBloggerPHID()); - if (!$blogger) { - return new Aphront404Response(); + $blogger = id(new PhabricatorUser())->loadOneWhere( + 'phid = %s', $post->getBloggerPHID()); + if (!$blogger) { + return new Aphront404Response(); + } } } else if ($this->getBloggerName() && $this->getPhameTitle()) { @@ -106,10 +106,12 @@ extends PhameController { if (!$blogger) { return new Aphront404Response(); } - $post = id(new PhamePost())->loadOneWhere( - 'bloggerPHID = %s AND phameTitle = %s', - $blogger->getPHID(), - $phame_title); + $posts = id(new PhamePostQuery()) + ->withBloggerPHID($blogger->getPHID()) + ->withPhameTitle($phame_title) + ->execute(); + $post = reset($posts); + if ($post && $phame_title != $this->getPhameTitle()) { $uri = $post->getViewURI($this->getBloggerName()); return id(new AphrontRedirectResponse())->setURI($uri); @@ -126,13 +128,24 @@ extends PhameController { } if ($post->isDraft()) { - $notice = id(new AphrontErrorView()) - ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) + $notice = $this->buildNoticeView() ->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 if ($request->getExists('saved')) { + $new_link = phutil_render_tag( + 'a', + array( + 'href' => '/phame/post/new/', + 'class' => 'button green', + ), + 'write another blog post' + ); + $notice = $this->buildNoticeView() + ->appendChild('

Saved post successfully.

') + ->appendChild('Seek even more phame and '.$new_link); } else { $notice = null; } diff --git a/src/applications/phame/controller/post/list/PhameAllBloggersPostListController.php b/src/applications/phame/controller/post/list/PhameAllPostListController.php similarity index 89% rename from src/applications/phame/controller/post/list/PhameAllBloggersPostListController.php rename to src/applications/phame/controller/post/list/PhameAllPostListController.php index c851ca092a..f21731a12a 100644 --- a/src/applications/phame/controller/post/list/PhameAllBloggersPostListController.php +++ b/src/applications/phame/controller/post/list/PhameAllPostListController.php @@ -19,7 +19,7 @@ /** * @group phame */ -final class PhameAllBloggersPostListController +final class PhameAllPostListController extends PhamePostListBaseController { public function shouldRequireLogin() { @@ -27,7 +27,7 @@ final class PhameAllBloggersPostListController } protected function getSideNavFilter() { - return 'everyone'; + return 'post/all'; } protected function getNoticeView() { @@ -65,9 +65,7 @@ final class PhameAllBloggersPostListController 'If you need more help try the '.$guide_link.'.', ); - $notice_view = id(new AphrontErrorView()) - ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) - ->setTitle('Meta thoughts and feelings'); + $notice_view = $this->buildNoticeView(); foreach ($notices as $notice) { $notice_view->appendChild('

'.$notice.'

'); } @@ -84,7 +82,7 @@ final class PhameAllBloggersPostListController $this->setActions(array('view')); - $page_title = 'Posts by Everyone'; + $page_title = 'All Posts'; $this->setPageTitle($page_title); $this->setShowSideNav(true); diff --git a/src/applications/phame/controller/post/list/PhameDraftListController.php b/src/applications/phame/controller/post/list/PhameDraftListController.php index 7eccbf9f14..1934e3d788 100644 --- a/src/applications/phame/controller/post/list/PhameDraftListController.php +++ b/src/applications/phame/controller/post/list/PhameDraftListController.php @@ -34,6 +34,19 @@ final class PhameDraftListController return true; } + protected function getNoticeView() { + $request = $this->getRequest(); + + if ($request->getExists('deleted')) { + $notice_view = $this->buildNoticeView() + ->appendChild('Deleted draft successfully.'); + } else { + $notice_view = null; + } + + return $notice_view; + } + public function processRequest() { $user = $this->getRequest()->getUser(); $phid = $user->getPHID(); diff --git a/src/applications/phame/controller/post/list/PhamePostListBaseController.php b/src/applications/phame/controller/post/list/PhamePostListBaseController.php index 21da11b2a5..93a42db8a7 100644 --- a/src/applications/phame/controller/post/list/PhamePostListBaseController.php +++ b/src/applications/phame/controller/post/list/PhamePostListBaseController.php @@ -54,17 +54,6 @@ abstract class PhamePostListBaseController return false; } - protected function getPager() { - $request = $this->getRequest(); - $pager = new AphrontPagerView(); - $page_size = 50; - $pager->setURI($request->getRequestURI(), 'offset'); - $pager->setPageSize($page_size); - $pager->setOffset($request->getInt('offset')); - - return $pager; - } - protected function getNoticeView() { return null; } @@ -102,7 +91,7 @@ abstract class PhamePostListBaseController $pager ), array( - 'title' => $this->getPageTitle(), + 'title' => $this->getPageTitle(), )); } } diff --git a/src/applications/phame/controller/post/list/PhameUserPostListController.php b/src/applications/phame/controller/post/list/PhameUserPostListController.php index ea25cf6390..cce488c889 100644 --- a/src/applications/phame/controller/post/list/PhameUserPostListController.php +++ b/src/applications/phame/controller/post/list/PhameUserPostListController.php @@ -31,38 +31,13 @@ final class PhameUserPostListController } protected function getNoticeView() { - $user = $this->getRequest()->getUser(); + $request = $this->getRequest(); - $new_link = phutil_render_tag( - 'a', - array( - 'href' => '/phame/post/new/', - 'class' => 'button green', - ), - 'write another blog post' - ); - - $pretty_uri = PhabricatorEnv::getProductionURI( - '/phame/posts/'.$user->getUserName().'/'); - $pretty_link = phutil_render_tag( - 'a', - array( - 'href' => (string) $pretty_uri - ), - (string) $pretty_uri - ); - - $notices = array( - 'Seek even more phame and '.$new_link, - 'Published posts also appear at the awesome, world-accessible '. - 'URI: '.$pretty_link - ); - - $notice_view = id(new AphrontErrorView()) - ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) - ->setTitle('Meta thoughts and feelings'); - foreach ($notices as $notice) { - $notice_view->appendChild('

'.$notice.'

'); + if ($request->getExists('deleted')) { + $notice_view = $this->buildNoticeView() + ->appendChild('Deleted post successfully.'); + } else { + $notice_view = null; } return $notice_view; diff --git a/src/applications/phame/query/PhameBlogQuery.php b/src/applications/phame/query/PhameBlogQuery.php new file mode 100644 index 0000000000..b68ed4894d --- /dev/null +++ b/src/applications/phame/query/PhameBlogQuery.php @@ -0,0 +1,108 @@ +phids = $phids; + return $this; + } + + public function needBloggers($need_bloggers) { + $this->needBloggers = $need_bloggers; + return $this; + } + + public function execute() { + $table = new PhameBlog(); + $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 b %Q %Q %Q', + $table->getTableName(), + $where_clause, + $order_clause, + $limit_clause); + + $blogs = $table->loadAllFromArray($data); + + if ($blogs) { + if ($this->needBloggers) { + $this->loadBloggers($blogs); + } + } + + return $blogs; + } + + private function loadBloggers(array $blogs) { + assert_instances_of($blogs, 'PhameBlog'); + $blog_phids = mpull($blogs, 'getPHID'); + + $edge_types = array(PhabricatorEdgeConfig::TYPE_BLOG_HAS_BLOGGER); + + $query = new PhabricatorEdgeQuery(); + $query->withSourcePHIDs($blog_phids) + ->withEdgeTypes($edge_types) + ->execute(); + + $all_blogger_phids = $query->getDestinationPHIDs( + $blog_phids, + $edge_types + ); + + $handles = id(new PhabricatorObjectHandleData($all_blogger_phids)) + ->loadHandles(); + + foreach ($blogs as $blog) { + $blogger_phids = $query->getDestinationPHIDs( + array($blog->getPHID()), + $edge_types + ); + $blog->attachBloggers(array_select_keys($handles, $blogger_phids)); + } + } + + private function buildWhereClause($conn_r) { + $where = array(); + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + return $this->formatWhereClause($where); + } + + private function buildOrderClause($conn_r) { + return 'ORDER BY id DESC'; + } +} diff --git a/src/applications/phame/query/PhamePostQuery.php b/src/applications/phame/query/PhamePostQuery.php index 31c587efcb..cfceec15a9 100644 --- a/src/applications/phame/query/PhamePostQuery.php +++ b/src/applications/phame/query/PhamePostQuery.php @@ -16,11 +16,16 @@ * limitations under the License. */ +/** + * @group phame + */ final class PhamePostQuery extends PhabricatorOffsetPagedQuery { private $bloggerPHID; private $withoutBloggerPHID; + private $phameTitle; private $visibility; + private $phids; /** * Mutually exlusive with @{method:withoutBloggerPHID}. @@ -37,11 +42,21 @@ final class PhamePostQuery extends PhabricatorOffsetPagedQuery { return $this; } + public function withPhameTitle($phame_title) { + $this->phameTitle = $phame_title; + return $this; + } + public function withVisibility($visibility) { $this->visibility = $visibility; return $this; } + public function withPHIDs($phids) { + $this->phids = $phids; + return $this; + } + public function execute() { $table = new PhamePost(); $conn_r = $table->establishConnection('r'); @@ -52,7 +67,7 @@ final class PhamePostQuery extends PhabricatorOffsetPagedQuery { $data = queryfx_all( $conn_r, - 'SELECT * FROM %T e %Q %Q %Q', + 'SELECT * FROM %T p %Q %Q %Q', $table->getTableName(), $where_clause, $order_clause, @@ -66,6 +81,14 @@ final class PhamePostQuery extends PhabricatorOffsetPagedQuery { private function buildWhereClause($conn_r) { $where = array(); + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids + ); + } + if ($this->bloggerPHID) { $where[] = qsprintf( $conn_r, @@ -80,6 +103,14 @@ final class PhamePostQuery extends PhabricatorOffsetPagedQuery { ); } + if ($this->phameTitle) { + $where[] = qsprintf( + $conn_r, + 'phameTitle = %s', + $this->phameTitle + ); + } + if ($this->visibility !== null) { $where[] = qsprintf( $conn_r, diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php new file mode 100644 index 0000000000..b5b5a3a52d --- /dev/null +++ b/src/applications/phame/storage/PhameBlog.php @@ -0,0 +1,135 @@ + true, + self::CONFIG_SERIALIZATION => array( + 'configData' => self::SERIALIZATION_JSON, + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorPHIDConstants::PHID_TYPE_BLOG); + } + + public function loadBloggerPHIDs() { + if (!$this->getPHID()) { + return $this; + } + + if ($this->bloggerPHIDs) { + return $this; + } + + $this->bloggerPHIDs = PhabricatorEdgeQuery::loadDestinationPHIDs( + $this->getPHID(), + PhabricatorEdgeConfig::TYPE_BLOG_HAS_BLOGGER + ); + + return $this; + } + + public function getBloggerPHIDs() { + if ($this->bloggerPHIDs === null) { + throw new Exception( + 'You must loadBloggerPHIDs before you can getBloggerPHIDs!' + ); + } + + return $this->bloggerPHIDs; + } + + public function loadBloggers() { + if ($this->bloggers) { + return $this->bloggers; + } + + $blogger_phids = $this->loadBloggerPHIDs()->getBloggerPHIDs(); + + if (empty($blogger_phids)) { + return array(); + } + + $bloggers = id(new PhabricatorObjectHandleData($blogger_phids)) + ->loadHandles(); + + $this->attachBloggers($bloggers); + + return $this; + } + + public function attachBloggers(array $bloggers) { + assert_instances_of($bloggers, 'PhabricatorObjectHandle'); + + $this->bloggers = $bloggers; + + return $this; + } + + public function getBloggers() { + if ($this->bloggers === null) { + throw new Exception( + 'You must loadBloggers or attachBloggers before you can getBloggers!' + ); + } + + return $this->bloggers; + } + + public function getPostListURI() { + return $this->getActionURI('posts'); + } + + public function getViewURI() { + return $this->getActionURI('view'); + } + + public function getEditURI() { + return $this->getActionURI('edit'); + } + + public function getEditFilter() { + return 'blog/edit/'.$this->getPHID(); + } + + public function getDeleteURI() { + return $this->getActionURI('delete'); + } + + private function getActionURI($action) { + return '/phame/blog/'.$action.'/'.$this->getPHID().'/'; + } +} diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index 95ede1be02..9abff35967 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -50,6 +50,9 @@ final class PhamePost extends PhameDAO { public function getDeleteURI() { return $this->getActionURI('delete'); } + public function getChangeVisibilityURI() { + return $this->getActionURI('changevisibility'); + } private function getActionURI($action) { return '/phame/post/'.$action.'/'.$this->getPHID().'/'; } @@ -58,6 +61,16 @@ final class PhamePost extends PhameDAO { return $this->getVisibility() == self::VISIBILITY_DRAFT; } + public function getHumanName() { + if ($this->isDraft()) { + $name = 'draft'; + } else { + $name = 'post'; + } + + return $name; + } + public function getCommentsWidget() { $config_data = $this->getConfigData(); if (empty($config_data)) { diff --git a/src/applications/phame/view/PhameBlogDetailView.php b/src/applications/phame/view/PhameBlogDetailView.php new file mode 100644 index 0000000000..ee29ccdecc --- /dev/null +++ b/src/applications/phame/view/PhameBlogDetailView.php @@ -0,0 +1,95 @@ +isAdmin = $is_admin; + return $this; + } + private function getIsAdmin() { + return $this->isAdmin; + } + + public function setUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + private function getUser() { + return $this->user; + } + + public function setBloggers(array $bloggers) { + assert_instances_of($bloggers, 'PhabricatorObjectHandle'); + $this->bloggers = $bloggers; + return $this; + } + private function getBloggers() { + return $this->bloggers; + } + + public function setBlog(PhameBlog $blog) { + $this->blog = $blog; + return $this; + } + private function getBlog() { + return $this->blog; + } + + + public function render() { + require_celerity_resource('phabricator-remarkup-css'); + + $user = $this->getUser(); + $blog = $this->getBlog(); + $bloggers = $this->getBloggers(); + $name = phutil_escape_html($blog->getName()); + $description = phutil_escape_html($blog->getDescription()); + $bloggers_txt = implode(' · ', mpull($bloggers, 'renderLink')); + $panel = id(new AphrontPanelView()) + ->setHeader($name) + ->setCaption($description) + ->setWidth(AphrontPanelView::WIDTH_FORM) + ->appendChild('Current bloggers: '.$bloggers_txt); + + if ($this->getIsAdmin()) { + $panel->addButton( + phutil_render_tag( + 'a', + array( + 'href' => $blog->getEditURI(), + 'class' => 'button grey', + ), + 'Edit Blog') + ); + + } + + return $panel->render(); + } + +} diff --git a/src/applications/phame/view/PhameBlogListView.php b/src/applications/phame/view/PhameBlogListView.php new file mode 100644 index 0000000000..452d73a088 --- /dev/null +++ b/src/applications/phame/view/PhameBlogListView.php @@ -0,0 +1,125 @@ +header = $header; + return $this; + } + private function getHeader() { + return $this->header; + } + + public function setUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + private function getUser() { + return $this->user; + } + + public function setBlogs(array $blogs) { + assert_instances_of($blogs, 'PhameBlog'); + $this->blogs = $blogs; + return $this; + } + private function getBlogs() { + return $this->blogs; + } + + public function render() { + $user = $this->getUser(); + $blogs = $this->getBlogs(); + $panel = new AphrontPanelView(); + + if (empty($blogs)) { + $panel = id(new AphrontPanelView()) + ->setHeader('No blogs... Yet!') + ->setCaption('Will you answer the call to phame?') + ->setCreateButton('New Blog', + '/phame/blog/new'); + return $panel->render(); + } + + $table_data = array(); + foreach ($blogs as $blog) { + $view_link = phutil_render_tag( + 'a', + array( + 'href' => $blog->getViewURI(), + ), + phutil_escape_html($blog->getName())); + $bloggers = $blog->getBloggers(); + if (isset($bloggers[$user->getPHID()])) { + $edit = phutil_render_tag( + 'a', + array( + 'class' => 'button small grey', + 'href' => $blog->getEditURI(), + ), + 'Edit'); + } else { + $edit = null; + } + $view = phutil_render_tag( + 'a', + array( + 'class' => 'button small grey', + 'href' => $blog->getViewURI(), + ), + 'View'); + $table_data[] = + array( + $view_link, + implode(', ', mpull($blog->getBloggers(), 'renderLink')), + $view, + $edit, + ); + } + + $table = new AphrontTableView($table_data); + $table->setHeaders( + array( + 'Name', + 'Bloggers', + '', + '', + )); + $table->setColumnClasses( + array( + null, + null, + 'action', + 'action', + )); + + $panel->setCreateButton('Create a Blog', '/phame/blog/new/'); + $panel->setHeader($this->getHeader()); + $panel->appendChild($table); + + return $panel->render(); + } +} diff --git a/src/applications/phid/PhabricatorPHIDConstants.php b/src/applications/phid/PhabricatorPHIDConstants.php index f8bd3985bb..55cde9bd0b 100644 --- a/src/applications/phid/PhabricatorPHIDConstants.php +++ b/src/applications/phid/PhabricatorPHIDConstants.php @@ -41,5 +41,5 @@ final class PhabricatorPHIDConstants { const PHID_TYPE_OASA = 'OASA'; const PHID_TYPE_POST = 'POST'; const PHID_TYPE_TOBJ = 'TOBJ'; - + const PHID_TYPE_BLOG = 'BLOG'; } diff --git a/src/docs/userguide/phame.diviner b/src/docs/userguide/phame.diviner index 4027074e84..1cf22b1b53 100644 --- a/src/docs/userguide/phame.diviner +++ b/src/docs/userguide/phame.diviner @@ -6,7 +6,39 @@ Journal about your thoughts and feelings. Share with others. Profit. = Overview = Phame is a simple blogging platform. You can write drafts which only you can -see. Later, you can publish these drafts as posts which anyone can see. +see. Later, you can publish these drafts as posts which anyone who can access +the Phabricator instance can see. You can also add posts to blogs to increase +your distribution. + +Overall, Phame is intended to help an individual spread their message. As +such, pertinent design decisions skew towards favoring the individual +rather than the collective. + += Drafts = + +Drafts are completely private so draft away. + += Posts = + +Posts are accessible to anyone who has access to the Phabricator instance. + += Blogs = + +Blogs are collections of posts. Each blog has associated metadata like +a name, description, and set of bloggers who can add posts to the blog. +Each blogger can also edit metadata about the blog and delete the blog +outright. + +Soon, blogs will be useful for powering external websites, like + + blog.yourcompany.com + +by making pertinent configuration changes with your DNS authority and +Phabricator instance. + +NOTE: removing a blogger from a given blog does not remove their posts that +are already associated with the blog. Rather, it removes their ability to edit +metadata about and add posts to the blog. = Comment Widgets = @@ -14,5 +46,11 @@ Phame supports comment widgets from Facebook and Disqus. The adminstrator of the Phabricator instance must properly configure Phabricator to enable this functionality. -NOTE: Phame is extremely new and very basic for now. Give us feedback on +A given comment widget is tied 1:1 with a given post. This means the same +instance of a given comment widget will appear for a given post regardless +of whether that post is being viewed in the context of a blog. + += Next Steps = + + - Phame is extremely new and very basic for now. Give us feedback on what you'd like to see improve! See @{article:Give Feedback! Get Support!}. diff --git a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php index 023829225c..7a08cddb20 100644 --- a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php +++ b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php @@ -30,6 +30,11 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { const TYPE_DREV_DEPENDS_ON_DREV = 5; const TYPE_DREV_DEPENDED_ON_BY_DREV = 6; + const TYPE_BLOG_HAS_POST = 7; + const TYPE_POST_HAS_BLOG = 8; + const TYPE_BLOG_HAS_BLOGGER = 9; + const TYPE_BLOGGER_HAS_BLOG = 10; + const TYPE_TEST_NO_CYCLE = 9000; public static function getInverse($edge_type) { @@ -42,6 +47,11 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { self::TYPE_DREV_DEPENDS_ON_DREV => self::TYPE_DREV_DEPENDED_ON_BY_DREV, self::TYPE_DREV_DEPENDED_ON_BY_DREV => self::TYPE_DREV_DEPENDS_ON_DREV, + + self::TYPE_BLOG_HAS_POST => self::TYPE_POST_HAS_BLOG, + self::TYPE_POST_HAS_BLOG => self::TYPE_BLOG_HAS_POST, + self::TYPE_BLOG_HAS_BLOGGER => self::TYPE_BLOGGER_HAS_BLOG, + self::TYPE_BLOGGER_HAS_BLOG => self::TYPE_BLOG_HAS_BLOGGER, ); return idx($map, $edge_type); @@ -67,6 +77,8 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { PhabricatorPHIDConstants::PHID_TYPE_MLST => 'PhabricatorMetaMTAMailingList', PhabricatorPHIDConstants::PHID_TYPE_TOBJ => 'HarbormasterObject', + PhabricatorPHIDConstants::PHID_TYPE_BLOG => 'PhameBlog', + PhabricatorPHIDConstants::PHID_TYPE_POST => 'PhamePost', ); $class = idx($class_map, $phid_type); @@ -79,5 +91,4 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { return newv($class, array())->establishConnection($conn_type); } - } diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index ecb847ebb6..ee04c3aa12 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -924,6 +924,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'name' => $this->getPatchPath( 'migrate-differential-dependencies.php'), ), + 'phameblog.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('phameblog.sql'), + ), ); } diff --git a/webroot/rsrc/js/application/phame/phame-post-blogs.js b/webroot/rsrc/js/application/phame/phame-post-blogs.js new file mode 100644 index 0000000000..c4f45f749c --- /dev/null +++ b/webroot/rsrc/js/application/phame/phame-post-blogs.js @@ -0,0 +1,23 @@ +/** + * @provides javelin-behavior-phame-post-blogs + * @requires javelin-behavior + * javelin-dom + */ + +JX.behavior('phame-post-blogs', function(config) { + + var visibility_select = JX.$(config.visibility.select_id); + var blogs_widget = JX.$(config.blogs.checkbox_id); + + var visibilityCallback = function(e) { + if (visibility_select.value == config.visibility.published) { + JX.DOM.show(blogs_widget); + } else { + JX.DOM.hide(blogs_widget); + } + e.kill(); + } + + JX.DOM.listen(visibility_select, 'change', null, visibilityCallback); + +});