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); + +});