diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php index e39176955a..1240351f11 100644 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -1,206 +1,210 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $post = id(new PhamePostQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->executeOne(); if (!$post) { return new Aphront404Response(); } $nav = $this->renderSideNavFilterView(); $this->loadHandles( array( $post->getBlogPHID(), $post->getBloggerPHID(), )); $actions = $this->renderActions($post, $user); $properties = $this->renderProperties($post, $user); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($post->getTitle()) ->setHref($this->getApplicationURI('post/view/'.$post->getID().'/'))); $nav->appendChild($crumbs); $nav->appendChild( id(new PhabricatorHeaderView()) ->setHeader($post->getTitle())); if ($post->isDraft()) { $nav->appendChild( id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle(pht('Draft Post')) ->appendChild( pht('Only you can see this draft until you publish it. '. 'Use "Preview / Publish" to publish this post.'))); } if (!$post->getBlog()) { $nav->appendChild( id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_WARNING) ->setTitle(pht('Not On A Blog')) ->appendChild( pht('This post is not associated with a blog (the blog may have '. 'been deleted). Use "Move Post" to move it to a new blog.'))); } $nav->appendChild( array( $actions, $properties, )); return $this->buildApplicationPage( $nav, array( 'title' => $post->getTitle(), 'device' => true, 'dust' => true, )); } private function renderActions( PhamePost $post, PhabricatorUser $user) { $actions = id(new PhabricatorActionListView()) ->setObject($post) ->setUser($user); $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $post, PhabricatorPolicyCapability::CAN_EDIT); $id = $post->getID(); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('edit') ->setHref($this->getApplicationURI('post/edit/'.$id.'/')) ->setName(pht('Edit Post')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('move') ->setHref($this->getApplicationURI('post/move/'.$id.'/')) ->setName(pht('Move Post')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($post->isDraft()) { $actions->addAction( id(new PhabricatorActionView()) ->setIcon('preview') ->setHref($this->getApplicationURI('post/publish/'.$id.'/')) ->setName(pht('Preview / Publish'))); } else { $actions->addAction( id(new PhabricatorActionView()) ->setIcon('unpublish') ->setHref($this->getApplicationURI('post/unpublish/'.$id.'/')) ->setName(pht('Unpublish')) ->setWorkflow(true)); } $actions->addAction( id(new PhabricatorActionView()) ->setIcon('delete') ->setHref($this->getApplicationURI('post/delete/'.$id.'/')) ->setName(pht('Delete Post')) ->setDisabled(!$can_edit) ->setWorkflow(true)); $blog = $post->getBlog(); $can_view_live = $blog && !$post->isDraft(); if ($can_view_live) { $live_uri = 'live/'.$blog->getID().'/post/'.$post->getPhameTitle(); } else { $live_uri = 'post/notlive/'.$post->getID().'/'; } $live_uri = $this->getApplicationURI($live_uri); $actions->addAction( id(new PhabricatorActionView()) ->setUser($user) ->setIcon('world') ->setHref($live_uri) ->setName(pht('View Live')) ->setRenderAsForm(true) ->setDisabled(!$can_view_live) ->setWorkflow(!$can_view_live)); return $actions; } private function renderProperties( PhamePost $post, PhabricatorUser $user) { - $properties = new PhabricatorPropertyListView(); + $properties = id(new PhabricatorPropertyListView()) + ->setUser($user) + ->setObject($post); $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $user, $post); $properties->addProperty( pht('Blog'), $post->getBlogPHID() ? $this->getHandle($post->getBlogPHID())->renderLink() : null); $properties->addProperty( pht('Blogger'), $this->getHandle($post->getBloggerPHID())->renderLink()); $properties->addProperty( pht('Visible To'), $descriptions[PhabricatorPolicyCapability::CAN_VIEW]); $properties->addProperty( pht('Published'), $post->isDraft() ? pht('Draft') : phabricator_datetime($post->getDatePublished(), $user)); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user) ->addObject($post, PhamePost::MARKUP_FIELD_BODY) ->process(); + $properties->invokeWillRenderEvent(); + $properties->addTextContent( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $engine->getOutput($post, PhamePost::MARKUP_FIELD_BODY))); return $properties; } } diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index 62414a60ec..3809679c94 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -1,185 +1,196 @@ blog = $blog; return $this; } public function getBlog() { return $this->blog; } public function getViewURI() { // go for the pretty uri if we can $domain = ($this->blog ? $this->blog->getDomain() : ''); if ($domain) { $phame_title = PhabricatorSlug::normalize($this->getPhameTitle()); return 'http://'.$domain.'/post/'.$phame_title; } $uri = '/phame/post/view/'.$this->getID().'/'; return PhabricatorEnv::getProductionURI($uri); } public function isDraft() { 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)) { return 'none'; } return idx($config_data, 'comments_widget', 'none'); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'configData' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_POST); } public static function getVisibilityOptionsForSelect() { return array( self::VISIBILITY_DRAFT => 'Draft: visible only to me.', self::VISIBILITY_PUBLISHED => 'Published: visible to the whole world.', ); } public function getCommentsWidgetOptionsForSelect() { $current = $this->getCommentsWidget(); $options = array(); if ($current == 'facebook' || PhabricatorEnv::getEnvConfig('facebook.application-id')) { $options['facebook'] = 'Facebook'; } if ($current == 'disqus' || PhabricatorEnv::getEnvConfig('disqus.shortname')) { $options['disqus'] = 'Disqus'; } $options['none'] = 'None'; return $options; } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { // Draft posts are visible only to the author. Published posts are visible // to whoever the blog is visible to. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if (!$this->isDraft() && $this->getBlog()) { return $this->getBlog()->getViewPolicy(); } else { return PhabricatorPolicies::POLICY_NOONE; } break; case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // A blog post's author can always view it, and is the only user allowed // to edit it. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: case PhabricatorPolicyCapability::CAN_EDIT: return ($user->getPHID() == $this->getBloggerPHID()); } } /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); return $this->getPHID().':'.$field.':'.$hash; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newPhameMarkupEngine(); } public function getMarkupText($field) { switch ($field) { case self::MARKUP_FIELD_BODY: return $this->getBody(); case self::MARKUP_FIELD_SUMMARY: return PhabricatorMarkupEngine::summarize($this->getBody()); } } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getPHID(); } +/* -( PhabricatorTokenReceiverInterface )---------------------------------- */ + + public function getUsersToNotifyOfTokenGiven() { + return array( + $this->getBloggerPHID(), + ); + } + } diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php index 3cb431dbee..58e1252dab 100644 --- a/src/applications/phriction/controller/PhrictionDocumentController.php +++ b/src/applications/phriction/controller/PhrictionDocumentController.php @@ -1,462 +1,454 @@ slug = $data['slug']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $slug = PhabricatorSlug::normalize($this->slug); if ($slug != $this->slug) { $uri = PhrictionDocument::getSlugURI($slug); // Canonicalize pages to their one true URI. return id(new AphrontRedirectResponse())->setURI($uri); } require_celerity_resource('phriction-document-css'); $document = id(new PhrictionDocument())->loadOneWhere( 'slug = %s', $slug); $version_note = null; $core_content = ''; - $byline = ''; $move_notice = ''; + $properties = null; if (!$document) { $document = new PhrictionDocument(); if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProject())->loadOneWhere( 'phrictionSlug = %s', PhrictionDocument::getProjectSlugIdentifier($slug)); if (!$project) { return new Aphront404Response(); } } $create_uri = '/phriction/edit/?slug='.$slug; - $no_content_head = pht('No content here!'); - $no_content_body = pht( - 'No document found at %s. You can '. - 'create a new document here.', - phutil_tag('tt', array(), $slug), - $create_uri); - - $no_content_text = hsprintf( - '%s
%s', - $no_content_head, - $no_content_body); - - $page_content = phutil_tag( - 'div', - array('class' => 'phriction-content'), - $no_content_text); + $notice = new AphrontErrorView(); + $notice->setSeverity(AphrontErrorView::SEVERITY_NODATA); + $notice->setTitle(pht('No content here!')); + $notice->appendChild( + pht( + 'No document found at %s. You can '. + 'create a new document here.', + phutil_tag('tt', array(), $slug), + $create_uri)); + $core_content = $notice; + $page_title = pht('Page Not Found'); } else { $version = $request->getInt('v'); if ($version) { $content = id(new PhrictionContent())->loadOneWhere( 'documentID = %d AND version = %d', $document->getID(), $version); if (!$content) { return new Aphront404Response(); } if ($content->getID() != $document->getContentID()) { $vdate = phabricator_datetime($content->getDateCreated(), $user); $version_note = new AphrontErrorView(); $version_note->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $version_note->setTitle('Older Version'); $version_note->appendChild( pht('You are viewing an older version of this document, as it '. 'appeared on %s.', $vdate)); } } else { $content = id(new PhrictionContent())->load($document->getContentID()); } $page_title = $content->getTitle(); - $project_phid = null; - if (PhrictionDocument::isProjectSlug($slug)) { - $project = id(new PhabricatorProject())->loadOneWhere( - 'phrictionSlug = %s', - PhrictionDocument::getProjectSlugIdentifier($slug)); - if ($project) { - $project_phid = $project->getPHID(); - } - } - $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $document->getPHID()); - - $phids = array_filter( - array( - $content->getAuthorPHID(), - $project_phid, - )); - - if ($subscribers) { - $phids = array_merge($phids, $subscribers); - } - - $handles = $this->loadViewerHandles($phids); - - $age = time() - $content->getDateCreated(); - $age = floor($age / (60 * 60 * 24)); - - if ($age < 1) { - $when = pht('today'); - } else if ($age == 1) { - $when = pht('yesterday'); - } else { - $when = pht("%d days ago", $age); - } - - - $project_info = null; - if ($project_phid) { - $project_info = hsprintf( - '
%s', - pht('This document is about the project %s.', - $handles[$project_phid]->renderLink())); - } - - $subscriber_view = null; - if ($subscribers) { - $subcriber_list = array(); - foreach ($subscribers as $subscriber) { - $subcriber_list[] = $handles[$subscriber]; - } - - $subcriber_list = phutil_implode_html(', ', - mpull($subcriber_list, 'renderLink')); - - $subscriber_view = array( - hsprintf('
Subscribers: '), - $subcriber_list, - ); - } - - $byline = hsprintf( - '
%s%s%s
', - pht('Last updated %s by %s.', - $when, - $handles[$content->getAuthorPHID()]->renderLink()), - $project_info, - $subscriber_view); - + $properties = $this + ->buildPropertyListView($document, $content, $slug, $subscribers); $doc_status = $document->getStatus(); $current_status = $content->getChangeType(); if ($current_status == PhrictionChangeType::CHANGE_EDIT || $current_status == PhrictionChangeType::CHANGE_MOVE_HERE) { $core_content = $content->renderContent($user); } else if ($current_status == PhrictionChangeType::CHANGE_DELETE) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle(pht('Document Deleted')); $notice->appendChild( pht('This document has been deleted. You can edit it to put new '. 'content here, or use history to revert to an earlier version.')); $core_content = $notice->render(); } else if ($current_status == PhrictionChangeType::CHANGE_STUB) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle(pht('Empty Document')); $notice->appendChild( pht('This document is empty. You can edit it to put some proper '. 'content here.')); $core_content = $notice->render(); } else if ($current_status == PhrictionChangeType::CHANGE_MOVE_AWAY) { $new_doc_id = $content->getChangeRef(); $new_doc = new PhrictionDocument(); $new_doc->load($new_doc_id); $slug_uri = PhrictionDocument::getSlugURI($new_doc->getSlug()); $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle(pht('Document Moved')); $notice->appendChild(phutil_tag('p', array(), pht('This document has been moved to %s. You can edit it to put new '. 'content here, or use history to revert to an earlier version.', phutil_tag('a', array('href' => $slug_uri), $slug_uri)))); $core_content = $notice->render(); } else { throw new Exception("Unknown document status '{$doc_status}'!"); } $move_notice = null; if ($current_status == PhrictionChangeType::CHANGE_MOVE_HERE) { $from_doc_id = $content->getChangeRef(); $from_doc = id(new PhrictionDocument())->load($from_doc_id); $slug_uri = PhrictionDocument::getSlugURI($from_doc->getSlug()); $move_notice = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->appendChild(pht('This document was moved from %s', phutil_tag('a', array('href' => $slug_uri), $slug_uri))) ->render(); } - } if ($version_note) { $version_note = $version_note->render(); } $children = $this->renderDocumentChildren($slug); $actions = $this->buildActionView($user, $document); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); $crumb_views = $this->renderBreadcrumbs($slug); foreach ($crumb_views as $view) { $crumbs->addCrumb($view); } $header = id(new PhabricatorHeaderView()) ->setHeader($page_title); - $page_content = hsprintf( - '
-
- %s%s%s%s%s -
-
-
', - $header, - $actions, - $byline, - $move_notice, - $core_content); + $page_content = hsprintf( + '
+
+ %s%s%s%s%s +
+
+
', + $header, + $actions, + $properties, + $move_notice, + $core_content); $core_page = phutil_tag( 'div', array( 'class' => 'phriction-offset' ), array( $page_content, $children, )); return $this->buildApplicationPage( array( $crumbs->render(), $core_page, ), array( 'title' => $page_title, 'device' => true, 'dust' => true, )); } + private function buildPropertyListView( + PhrictionDocument $document, + PhrictionContent $content, + $slug, + array $subscribers) { + + $viewer = $this->getRequest()->getUser(); + $view = id(new PhabricatorPropertyListView()) + ->setUser($viewer) + ->setObject($document); + + $project_phid = null; + if (PhrictionDocument::isProjectSlug($slug)) { + $project = id(new PhabricatorProject())->loadOneWhere( + 'phrictionSlug = %s', + PhrictionDocument::getProjectSlugIdentifier($slug)); + if ($project) { + $project_phid = $project->getPHID(); + } + } + + $phids = array_filter( + array( + $content->getAuthorPHID(), + $project_phid, + )); + + if ($subscribers) { + $phids = array_merge($phids, $subscribers); + } + + $this->loadHandles($phids); + + $project_info = null; + if ($project_phid) { + $view->addProperty( + pht('Project Info'), + $this->getHandle($project_phid)->renderLink()); + } + + $view->addProperty( + pht('Last Author'), + $this->getHandle($content->getAuthorPHID())->renderLink()); + + $age = time() - $content->getDateCreated(); + $age = floor($age / (60 * 60 * 24)); + if ($age < 1) { + $when = pht('Today'); + } else if ($age == 1) { + $when = pht('Yesterday'); + } else { + $when = pht("%d Days Ago", $age); + } + $view->addProperty(pht('Last Updated'), $when); + + if ($subscribers) { + $subscribers = $this->renderHandlesForPHIDs($subscribers); + $view->addProperty(pht('Subscribers'), $subscribers); + } + + return $view; + } + private function buildActionView( PhabricatorUser $user, PhrictionDocument $document) { $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $document, PhabricatorPolicyCapability::CAN_EDIT); $slug = PhabricatorSlug::normalize($this->slug); $action_view = id(new PhabricatorActionListView()) ->setUser($user) ->setObject($document); if (!$document->getID()) { return $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('Create This Document')) ->setIcon('create') ->setHref('/phriction/edit/?slug='.$slug)); } $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Document')) ->setIcon('edit') ->setHref('/phriction/edit/'.$document->getID().'/')); if ($document->getStatus() == PhrictionDocumentStatus::STATUS_EXISTS) { $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('Move Document')) ->setIcon('move') ->setHref('/phriction/move/'.$document->getID().'/') ->setWorkflow(true)); $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('Delete Document')) ->setIcon('delete') ->setHref('/phriction/delete/'.$document->getID().'/') ->setWorkflow(true)); } return $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('View History')) ->setIcon('history') ->setHref(PhrictionDocument::getSlugURI($slug, 'history'))); } private function renderDocumentChildren($slug) { $document_dao = new PhrictionDocument(); $content_dao = new PhrictionContent(); $conn = $document_dao->establishConnection('r'); $limit = 50; $d_child = PhabricatorSlug::getDepth($slug) + 1; $d_grandchild = PhabricatorSlug::getDepth($slug) + 2; // Select children and grandchildren. $children = queryfx_all( $conn, 'SELECT d.slug, d.depth, c.title FROM %T d JOIN %T c ON d.contentID = c.id WHERE d.slug LIKE %> AND d.depth IN (%d, %d) AND d.status IN (%Ld) ORDER BY d.depth, c.title LIMIT %d', $document_dao->getTableName(), $content_dao->getTableName(), ($slug == '/' ? '' : $slug), $d_child, $d_grandchild, array( PhrictionDocumentStatus::STATUS_EXISTS, PhrictionDocumentStatus::STATUS_STUB, ), $limit); if (!$children) { return; } // We're going to render in one of three modes to try to accommodate // different information scales: // // - If we found fewer than $limit rows, we know we have all the children // and grandchildren and there aren't all that many. We can just render // everything. // - If we found $limit rows but the results included some grandchildren, // we just throw them out and render only the children, as we know we // have them all. // - If we found $limit rows and the results have no grandchildren, we // have a ton of children. Render them and then let the user know that // this is not an exhaustive list. if (count($children) == $limit) { $more_children = true; foreach ($children as $child) { if ($child['depth'] == $d_grandchild) { $more_children = false; } } $show_grandchildren = false; } else { $show_grandchildren = true; $more_children = false; } $grandchildren = array(); foreach ($children as $key => $child) { if ($child['depth'] == $d_child) { continue; } else { unset($children[$key]); if ($show_grandchildren) { $ancestors = PhabricatorSlug::getAncestry($child['slug']); $grandchildren[end($ancestors)][] = $child; } } } // Fill in any missing children. $known_slugs = ipull($children, null, 'slug'); foreach ($grandchildren as $slug => $ignored) { if (empty($known_slugs[$slug])) { $children[] = array( 'slug' => $slug, 'depth' => $d_child, 'title' => PhabricatorSlug::getDefaultTitle($slug), 'empty' => true, ); } } $children = isort($children, 'title'); $list = array(); foreach ($children as $child) { $list[] = hsprintf('
  • '); $list[] = $this->renderChildDocumentLink($child); $grand = idx($grandchildren, $child['slug'], array()); if ($grand) { $list[] = hsprintf(''); } $list[] = hsprintf('
  • '); } if ($more_children) { $list[] = phutil_tag('li', array(), pht('More...')); } return hsprintf( '
    %s
    %s
    ', pht('Document Hierarchy'), phutil_tag('ul', array(), $list)); } private function renderChildDocumentLink(array $info) { $title = nonempty($info['title'], pht('(Untitled Document)')); $item = phutil_tag( 'a', array( 'href' => PhrictionDocument::getSlugURI($info['slug']), ), $title); if (isset($info['empty'])) { $item = phutil_tag('em', array(), $item); } return $item; } protected function getDocumentSlug() { return $this->slug; } } diff --git a/src/applications/phriction/storage/PhrictionDocument.php b/src/applications/phriction/storage/PhrictionDocument.php index f3bc38a705..af74769f29 100644 --- a/src/applications/phriction/storage/PhrictionDocument.php +++ b/src/applications/phriction/storage/PhrictionDocument.php @@ -1,132 +1,141 @@ true, self::CONFIG_TIMESTAMPS => false, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_WIKI); } public static function getSlugURI($slug, $type = 'document') { static $types = array( 'document' => '/w/', 'history' => '/phriction/history/', ); if (empty($types[$type])) { throw new Exception("Unknown URI type '{$type}'!"); } $prefix = $types[$type]; if ($slug == '/') { return $prefix; } else { // NOTE: The effect here is to escape non-latin characters, since modern // browsers deal with escaped UTF8 characters in a reasonable way (showing // the user a readable URI) but older programs may not. $slug = phutil_escape_uri($slug); return $prefix.$slug; } } public function setSlug($slug) { $this->slug = PhabricatorSlug::normalize($slug); $this->depth = PhabricatorSlug::getDepth($slug); return $this; } public function attachContent(PhrictionContent $content) { $this->contentObject = $content; return $this; } public function getContent() { if (!$this->contentObject) { throw new Exception("Attach content with attachContent() first."); } return $this->contentObject; } public function getProject() { if ($this->project === null) { throw new Exception("Call attachProject() before getProject()."); } return $this->project; } public function attachProject(PhabricatorProject $project) { $this->project = $project; return $this; } public function hasProject() { return (bool)$this->project; } public static function isProjectSlug($slug) { $slug = PhabricatorSlug::normalize($slug); $prefix = 'projects/'; if ($slug == $prefix) { // The 'projects/' document is not itself a project slug. return false; } return !strncmp($slug, $prefix, strlen($prefix)); } public static function getProjectSlugIdentifier($slug) { if (!self::isProjectSlug($slug)) { throw new Exception("Slug '{$slug}' is not a project slug!"); } $slug = PhabricatorSlug::normalize($slug); $parts = explode('/', $slug); return $parts[1].'/'; } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { if ($this->hasProject()) { return $this->getProject()->getPolicy($capability); } return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $user) { if ($this->hasProject()) { return $this->getProject()->hasAutomaticCapability($capability, $user); } return false; } public function isAutomaticallySubscribed($phid) { return false; } + +/* -( PhabricatorTokenReceiverInterface )---------------------------------- */ + + public function getUsersToNotifyOfTokenGiven() { + return PhabricatorSubscribersQuery::loadSubscribersForPHID($this->phid); + } } diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php index 758a0a6673..29ad9532c1 100644 --- a/src/applications/ponder/controller/PonderQuestionViewController.php +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -1,136 +1,137 @@ questionID = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $question = PonderQuestionQuery::loadSingle($user, $this->questionID); if (!$question) { return new Aphront404Response(); } $question->attachRelated(); $question->attachVotes($user->getPHID()); $object_phids = array($user->getPHID(), $question->getAuthorPHID()); $answers = $question->getAnswers(); $comments = $question->getComments(); foreach ($comments as $comment) { $object_phids[] = $comment->getAuthorPHID(); } foreach ($answers as $answer) { $object_phids[] = $answer->getAuthorPHID(); $comments = $answer->getComments(); foreach ($comments as $comment) { $object_phids[] = $comment->getAuthorPHID(); } } $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $question->getPHID()); $object_phids = array_merge($object_phids, $subscribers); $this->loadHandles($object_phids); $handles = $this->getLoadedHandles(); $detail_panel = new PonderQuestionDetailView(); $detail_panel ->setQuestion($question) ->setUser($user) ->setHandles($handles); $responses_panel = new PonderAnswerListView(); $responses_panel ->setQuestion($question) ->setHandles($handles) ->setUser($user) ->setAnswers($answers); $answer_add_panel = new PonderAddAnswerView(); $answer_add_panel ->setQuestion($question) ->setUser($user) ->setActionURI("/ponder/answer/add/"); $header = id(new PhabricatorHeaderView()) ->setHeader($question->getTitle()); $actions = $this->buildActionListView($question); $properties = $this->buildPropertyListView($question, $subscribers); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); $crumbs->setActionList($actions); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName('Q'.$this->questionID) ->setHref('/Q'.$this->questionID)); $nav = $this->buildSideNavView($question); $nav->appendChild( array( $crumbs, $header, $actions, $properties, $detail_panel, $responses_panel, $answer_add_panel )); $nav->selectFilter(null); return $this->buildApplicationPage( $nav, array( 'device' => true, 'title' => 'Q'.$question->getID().' '.$question->getTitle() )); } private function buildActionListView(PonderQuestion $question) { $viewer = $this->getRequest()->getUser(); $view = new PhabricatorActionListView(); $view->setUser($viewer); $view->setObject($question); return $view; } private function buildPropertyListView( PonderQuestion $question, array $subscribers) { $viewer = $this->getRequest()->getUser(); - $view = new PhabricatorPropertyListView(); - + $view = id(new PhabricatorPropertyListView()) + ->setUser($viewer) + ->setObject($question); $view->addProperty( pht('Author'), $this->getHandle($question->getAuthorPHID())->renderLink()); $view->addProperty( pht('Created'), phabricator_datetime($question->getDateCreated(), $viewer)); if ($subscribers) { $subscribers = $this->renderHandlesForPHIDs($subscribers); } $view->addProperty( pht('Subscribers'), nonempty($subscribers, phutil_tag('em', array(), pht('None')))); return $view; } } diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php index 9028681059..88160a35a1 100644 --- a/src/applications/ponder/storage/PonderQuestion.php +++ b/src/applications/ponder/storage/PonderQuestion.php @@ -1,193 +1,202 @@ true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_QUES); } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function attachRelated() { $this->answers = $this->loadRelatives(new PonderAnswer(), "questionID"); $qa_phids = mpull($this->answers, 'getPHID') + array($this->getPHID()); if ($qa_phids) { $comments = id(new PonderCommentQuery()) ->withTargetPHIDs($qa_phids) ->execute(); $comments = mgroup($comments, 'getTargetPHID'); } else { $comments = array(); } $this->setComments(idx($comments, $this->getPHID(), array())); foreach ($this->answers as $answer) { $answer->setQuestion($this); $answer->setComments(idx($comments, $answer->getPHID(), array())); } } public function attachVotes($user_phid) { $qa_phids = mpull($this->answers, 'getPHID') + array($this->getPHID()); $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($user_phid)) ->withDestinationPHIDs($qa_phids) ->withEdgeTypes( array( PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_QUESTION, PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_ANSWER )) ->needEdgeData(true) ->execute(); $question_edge = $edges[$user_phid][PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_QUESTION]; $answer_edges = $edges[$user_phid][PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_ANSWER]; $edges = null; $this->setUserVote(idx($question_edge, $this->getPHID())); foreach ($this->answers as $answer) { $answer->setUserVote(idx($answer_edges, $answer->getPHID())); } } public function setUserVote($vote) { $this->vote = $vote['data']; if (!$this->vote) { $this->vote = PonderConstants::NONE_VOTE; } return $this; } public function getUserVote() { return $this->vote; } public function setComments($comments) { $this->comments = $comments; return $this; } public function getComments() { return $this->comments; } public function getAnswers() { return $this->answers; } public function getMarkupField() { return self::MARKUP_FIELD_CONTENT; } // Markup interface public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "ponder:Q{$id}:{$field}:{$hash}"; } public function getMarkupText($field) { return $this->getContent(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newPonderMarkupEngine(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } // votable interface public function getUserVoteEdgeType() { return PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_QUESTION; } public function getVotablePHID() { return $this->getPHID(); } public function isAutomaticallySubscribed($phid) { return false; } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { $policy = PhabricatorPolicies::POLICY_NOONE; switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $policy = PhabricatorPolicies::POLICY_USER; break; } return $policy; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } +/* -( PhabricatorTokenReceiverInterface )---------------------------------- */ + + public function getUsersToNotifyOfTokenGiven() { + return array( + $this->getAuthorPHID(), + ); + } + } diff --git a/src/applications/subscriptions/query/PhabricatorSubscribersQuery.php b/src/applications/subscriptions/query/PhabricatorSubscribersQuery.php index 0870fe27ee..73a5bf5a4b 100644 --- a/src/applications/subscriptions/query/PhabricatorSubscribersQuery.php +++ b/src/applications/subscriptions/query/PhabricatorSubscribersQuery.php @@ -1,54 +1,52 @@ withObjectPHIDs(array($phid)) ->execute(); return $subscribers[$phid]; } public function withObjectPHIDs(array $object_phids) { $this->objectPHIDs = $object_phids; return $this; } public function withSubscriberPHIDs(array $subscriber_phids) { $this->subscriberPHIDs = $subscriber_phids; return $this; } public function execute() { $query = new PhabricatorEdgeQuery(); $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_SUBSCRIBER; $query->withSourcePHIDs($this->objectPHIDs); $query->withEdgeTypes(array($edge_type)); if ($this->subscriberPHIDs) { $query->withDestinationPHIDs($this->subscriberPHIDs); } $edges = $query->execute(); $results = array_fill_keys($this->objectPHIDs, array()); foreach ($edges as $src => $edge_types) { foreach ($edge_types[$edge_type] as $dst => $data) { $results[$src][] = $dst; } } return $results; } - - } diff --git a/webroot/rsrc/css/application/phriction/phriction-document-css.css b/webroot/rsrc/css/application/phriction/phriction-document-css.css index f2ea174839..899ae2b476 100644 --- a/webroot/rsrc/css/application/phriction/phriction-document-css.css +++ b/webroot/rsrc/css/application/phriction/phriction-document-css.css @@ -1,163 +1,152 @@ /** * @provides phriction-document-css */ .phriction-header { background: #eeeeee; border-bottom: 1px solid #dddddd; padding: 1em; } .phriction-header a.button { float: right; margin: 0em 0em 0em 1%; } .phriction-header h1 { margin: .25em 0; } .device-desktop .phriction-offset { padding-right: 160px; } .phriction-wrap { margin-bottom: 20px; } .device-desktop .phriction-wrap { position: relative; border-left: 1px solid #e7e7e7; border-right: 1px solid #e7e7e7; border-bottom: 1px solid #c0c5d1; max-width: 800px; margin: 20px auto; } .phriction-content { box-shadow: 0 1px 2px rgba(0,0,0,0.2); min-height: 240px; background: #fff; } .device-desktop .phriction-wrap { } .device-desktop .phriction-content .phabricator-action-list-view { margin: 10px 10px 0 0; background: #f7f7f7; } .device-phone .phriction-content .phabricator-action-list-view { margin: 0; border-bottom: 1px solid #c0c5d1; background: #f7f7f7; } .phriction-content .phabricator-header-shell { border-top: none; } .phriction-content .phabricator-remarkup { padding: 20px; } -.phriction-byline { - padding: 10px 20px; - color: #777; - font-size: 11px; - background: #f7f7f7; -} - -.device-phone .phriction-byline { - padding: 10px; -} - .device-phone .phriction-content .phabricator-remarkup { padding: 10px; } .phriction-content .phriction-link { font-weight: bold; } .phriction-breadcrumbs { font-size: 12px; color: #666666; } .phriction-document-crumbs a { font-weight: bold; } .phriction-children { max-width: 800px; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.2); } .phriction-children ul { margin-left: 30px; padding-bottom: 10px; list-style: circle; color: #999999; } .phriction-children-header { background: #f7f7f7; padding: 10px 0 10px 20px; font-weight: bold; margin-bottom: 15px; } .device-desktop .phriction-content .phabricator-action-list-view { position: absolute; top: 50px; right: -172px; float: none; background: #fff; border-radius: 0; box-shadow: none; border: none; border-top: 1px solid #e7e7e7; border-bottom: 1px solid #e7e7e7; border-right: 1px solid #e7e7e7; width: 160px; } .phriction-document-preview-header { color: #666666; margin-bottom: 1em; font-size: 11px; } .phriction-document-history-diff { padding: 0 2em 2em; } .phriction-revert-table { width: 100%; } .phriction-revert-table td { text-align: center; width: 50%; padding: .5em 0; } .phriction-history-nav-table { width: 100%; } .phriction-history-nav-table td { width: 50%; color: #444444; } .phriction-history-nav-table td.nav-next { text-align: right; } .device-phone .phriction-content .phabricator-remarkup-toc { width: 120px; }