1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-26 07:20:57 +01:00

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
This commit is contained in:
Bob Trahan 2012-07-19 09:03:10 -07:00
parent ee709a0543
commit ae13d33859
29 changed files with 1711 additions and 134 deletions

View file

@ -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`);

View file

@ -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',
),
));

View file

@ -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',

View file

@ -382,7 +382,7 @@ class AphrontDefaultApplicationConfiguration
),
'/phame/' => array(
'' => 'PhameAllBloggersPostListController',
'' => 'PhameAllPostListController',
'post/' => array(
'' => 'PhameUserPostListController',
'delete/(?P<phid>[^/]+)/' => 'PhamePostDeleteController',
@ -395,6 +395,14 @@ class AphrontDefaultApplicationConfiguration
'' => 'PhameDraftListController',
'new/' => 'PhamePostEditController',
),
'blog/' => array(
'' => 'PhameUserBlogListController',
'all/' => 'PhameAllBlogListController',
'new/' => 'PhameBlogEditController',
'delete/(?P<phid>[^/]+)/' => 'PhameBlogDeleteController',
'edit/(?P<phid>[^/]+)/' => 'PhameBlogEditController',
'view/(?P<phid>[^/]+)/' => 'PhameBlogViewController',
),
'posts/' => array(
'' => 'PhameUserPostListController',
'(?P<bloggername>\w+)/' => 'PhameBloggerPostListController',

View file

@ -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;
}
}

View file

@ -0,0 +1,115 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group phame
*/
final class PhameBlogDeleteController
extends PhameController {
private $phid;
private function setBlogPHID($phid) {
$this->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);
}
}

View file

@ -0,0 +1,248 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group phame
*/
final class PhameBlogEditController
extends PhameController {
private $phid;
private $isBlogEdit;
private function setBlogPHID($phid) {
$this->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,
));
}
}

View file

@ -0,0 +1,170 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group phame
*/
final class PhameBlogViewController
extends PhameController {
private $blogPHID;
private $bloggerPHIDs;
private $postPHIDs;
private function setPostPHIDs($post_phids) {
$this->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);
}
}

View file

@ -0,0 +1,45 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group phame
*/
final class PhameAllBlogListController
extends PhameBlogListBaseController {
protected function getSideNavFilter() {
return 'blog/all';
}
public function processRequest() {
$user = $this->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();
}
}

View file

@ -0,0 +1,71 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group phame
*/
abstract class PhameBlogListBaseController
extends PhameController {
private $blogs;
private $pageTitle;
protected function setBlogs(array $blogs) {
assert_instances_of($blogs, 'PhameBlog');
$this->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(),
));
}
}

View file

@ -0,0 +1,65 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group phame
*/
final class PhameUserBlogListController
extends PhameBlogListBaseController {
protected function getNoticeView() {
$request = $this->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();
}
}

View file

@ -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);

View file

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

View file

@ -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('<p>Saved post successfully.</p>')
->appendChild('Seek even more phame and '.$new_link);
} else {
$notice = null;
}

View file

@ -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('<p>'.$notice.'</p>');
}
@ -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);

View file

@ -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();

View file

@ -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(),
));
}
}

View file

@ -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('<p>'.$notice.'</p>');
if ($request->getExists('deleted')) {
$notice_view = $this->buildNoticeView()
->appendChild('Deleted post successfully.');
} else {
$notice_view = null;
}
return $notice_view;

View file

@ -0,0 +1,108 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group phame
*/
final class PhameBlogQuery extends PhabricatorOffsetPagedQuery {
private $phids;
private $needBloggers;
public function withPHIDs($phids) {
$this->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';
}
}

View file

@ -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,

View file

@ -0,0 +1,135 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group phame
*/
final class PhameBlog extends PhameDAO {
protected $id;
protected $phid;
protected $name;
protected $description;
protected $configData;
protected $creatorPHID;
private $bloggerPHIDs;
private $bloggers;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'configData' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_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().'/';
}
}

View file

@ -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)) {

View file

@ -0,0 +1,95 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group phame
*/
final class PhameBlogDetailView extends AphrontView {
private $user;
private $isAdmin;
private $blog;
private $bloggers;
public function setIsAdmin($is_admin) {
$this->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(' &middot; ', 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();
}
}

View file

@ -0,0 +1,125 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group phame
*/
final class PhameBlogListView extends AphrontView {
private $user;
private $blogs;
private $header;
public function setHeader($header) {
$this->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();
}
}

View file

@ -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';
}

View file

@ -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!}.

View file

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

View file

@ -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'),
),
);
}

View file

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