Add ability to set a header image per Phame blog
Summary: This is the backend half of uploading an image as a header for Phame Blogs. Allows you to upload image, or delete it. Ref T10901
Test Plan:
Go to Manage Blog, visit Edit Header Image, Upload snarky file. See snarky file on Manage page. Edit Header Image, click delete, save, see file goes away.
{F1690966}
Reviewers: epriestley
Reviewed By: epriestley
Subscribers: Korvin
Maniphest Tasks: T10901
Differential Revision: https://secure.phabricator.com/D16140
			
			
This commit is contained in:
		| @@ -7,7 +7,7 @@ | |||||||
|  */ |  */ | ||||||
| return array( | return array( | ||||||
|   'names' => array( |   'names' => array( | ||||||
|     'core.pkg.css' => '6913fe66', |     'core.pkg.css' => 'c7fc5aec', | ||||||
|     'core.pkg.js' => '10275c16', |     'core.pkg.js' => '10275c16', | ||||||
|     'darkconsole.pkg.js' => 'e7393ebb', |     'darkconsole.pkg.js' => 'e7393ebb', | ||||||
|     'differential.pkg.css' => 'b3eea3f5', |     'differential.pkg.css' => 'b3eea3f5', | ||||||
| @@ -155,7 +155,7 @@ return array( | |||||||
|     'rsrc/css/phui/phui-spacing.css' => '042804d6', |     'rsrc/css/phui/phui-spacing.css' => '042804d6', | ||||||
|     'rsrc/css/phui/phui-status.css' => 'd5263e49', |     'rsrc/css/phui/phui-status.css' => 'd5263e49', | ||||||
|     'rsrc/css/phui/phui-tag-view.css' => '6bbd83e2', |     'rsrc/css/phui/phui-tag-view.css' => '6bbd83e2', | ||||||
|     'rsrc/css/phui/phui-timeline-view.css' => '6e342216', |     'rsrc/css/phui/phui-timeline-view.css' => '8ea41b25', | ||||||
|     'rsrc/css/phui/phui-two-column-view.css' => '9fb86c85', |     'rsrc/css/phui/phui-two-column-view.css' => '9fb86c85', | ||||||
|     'rsrc/css/phui/workboards/phui-workboard-color.css' => 'ac6fe6a7', |     'rsrc/css/phui/workboards/phui-workboard-color.css' => 'ac6fe6a7', | ||||||
|     'rsrc/css/phui/workboards/phui-workboard.css' => 'e6d89647', |     'rsrc/css/phui/workboards/phui-workboard.css' => 'e6d89647', | ||||||
| @@ -860,7 +860,7 @@ return array( | |||||||
|     'phui-status-list-view-css' => 'd5263e49', |     'phui-status-list-view-css' => 'd5263e49', | ||||||
|     'phui-tag-view-css' => '6bbd83e2', |     'phui-tag-view-css' => '6bbd83e2', | ||||||
|     'phui-theme-css' => '027ba77e', |     'phui-theme-css' => '027ba77e', | ||||||
|     'phui-timeline-view-css' => '6e342216', |     'phui-timeline-view-css' => '8ea41b25', | ||||||
|     'phui-two-column-view-css' => '9fb86c85', |     'phui-two-column-view-css' => '9fb86c85', | ||||||
|     'phui-workboard-color-css' => 'ac6fe6a7', |     'phui-workboard-color-css' => 'ac6fe6a7', | ||||||
|     'phui-workboard-view-css' => 'e6d89647', |     'phui-workboard-view-css' => 'e6d89647', | ||||||
|   | |||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | ALTER TABLE {$NAMESPACE}_phame.phame_blog | ||||||
|  |   ADD headerImagePHID VARBINARY(64); | ||||||
| @@ -3777,6 +3777,7 @@ phutil_register_library_map(array( | |||||||
|     'PhameBlogEditor' => 'applications/phame/editor/PhameBlogEditor.php', |     'PhameBlogEditor' => 'applications/phame/editor/PhameBlogEditor.php', | ||||||
|     'PhameBlogFeedController' => 'applications/phame/controller/blog/PhameBlogFeedController.php', |     'PhameBlogFeedController' => 'applications/phame/controller/blog/PhameBlogFeedController.php', | ||||||
|     'PhameBlogFulltextEngine' => 'applications/phame/search/PhameBlogFulltextEngine.php', |     'PhameBlogFulltextEngine' => 'applications/phame/search/PhameBlogFulltextEngine.php', | ||||||
|  |     'PhameBlogHeaderPictureController' => 'applications/phame/controller/blog/PhameBlogHeaderPictureController.php', | ||||||
|     'PhameBlogListController' => 'applications/phame/controller/blog/PhameBlogListController.php', |     'PhameBlogListController' => 'applications/phame/controller/blog/PhameBlogListController.php', | ||||||
|     'PhameBlogListView' => 'applications/phame/view/PhameBlogListView.php', |     'PhameBlogListView' => 'applications/phame/view/PhameBlogListView.php', | ||||||
|     'PhameBlogManageController' => 'applications/phame/controller/blog/PhameBlogManageController.php', |     'PhameBlogManageController' => 'applications/phame/controller/blog/PhameBlogManageController.php', | ||||||
| @@ -8655,6 +8656,7 @@ phutil_register_library_map(array( | |||||||
|     'PhameBlogEditor' => 'PhabricatorApplicationTransactionEditor', |     'PhameBlogEditor' => 'PhabricatorApplicationTransactionEditor', | ||||||
|     'PhameBlogFeedController' => 'PhameBlogController', |     'PhameBlogFeedController' => 'PhameBlogController', | ||||||
|     'PhameBlogFulltextEngine' => 'PhabricatorFulltextEngine', |     'PhameBlogFulltextEngine' => 'PhabricatorFulltextEngine', | ||||||
|  |     'PhameBlogHeaderPictureController' => 'PhameBlogController', | ||||||
|     'PhameBlogListController' => 'PhameBlogController', |     'PhameBlogListController' => 'PhameBlogController', | ||||||
|     'PhameBlogListView' => 'AphrontTagView', |     'PhameBlogListView' => 'AphrontTagView', | ||||||
|     'PhameBlogManageController' => 'PhameBlogController', |     'PhameBlogManageController' => 'PhameBlogController', | ||||||
|   | |||||||
| @@ -67,6 +67,7 @@ final class PhabricatorPhameApplication extends PhabricatorApplication { | |||||||
|           'manage/(?P<id>[^/]+)/' => 'PhameBlogManageController', |           'manage/(?P<id>[^/]+)/' => 'PhameBlogManageController', | ||||||
|           'feed/(?P<id>[^/]+)/' => 'PhameBlogFeedController', |           'feed/(?P<id>[^/]+)/' => 'PhameBlogFeedController', | ||||||
|           'picture/(?P<id>[1-9]\d*)/' => 'PhameBlogProfilePictureController', |           'picture/(?P<id>[1-9]\d*)/' => 'PhameBlogProfilePictureController', | ||||||
|  |           'header/(?P<id>[1-9]\d*)/' => 'PhameBlogHeaderPictureController', | ||||||
|         ), |         ), | ||||||
|       ) + $this->getResourceSubroutes(), |       ) + $this->getResourceSubroutes(), | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -0,0 +1,126 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | final class PhameBlogHeaderPictureController | ||||||
|  |   extends PhameBlogController { | ||||||
|  |  | ||||||
|  |   public function handleRequest(AphrontRequest $request) { | ||||||
|  |     $viewer = $request->getViewer(); | ||||||
|  |     $id = $request->getURIData('id'); | ||||||
|  |  | ||||||
|  |     $blog = id(new PhameBlogQuery()) | ||||||
|  |       ->setViewer($viewer) | ||||||
|  |       ->withIDs(array($id)) | ||||||
|  |       ->needHeaderImage(true) | ||||||
|  |       ->requireCapabilities( | ||||||
|  |         array( | ||||||
|  |           PhabricatorPolicyCapability::CAN_VIEW, | ||||||
|  |           PhabricatorPolicyCapability::CAN_EDIT, | ||||||
|  |         )) | ||||||
|  |       ->executeOne(); | ||||||
|  |     if (!$blog) { | ||||||
|  |       return new Aphront404Response(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     $blog_uri = '/phame/blog/manage/'.$id; | ||||||
|  |  | ||||||
|  |     $supported_formats = PhabricatorFile::getTransformableImageFormats(); | ||||||
|  |     $e_file = true; | ||||||
|  |     $errors = array(); | ||||||
|  |     $delete_header = ($request->getInt('delete') == 1); | ||||||
|  |  | ||||||
|  |     if ($request->isFormPost()) { | ||||||
|  |       if ($request->getFileExists('header')) { | ||||||
|  |         $file = PhabricatorFile::newFromPHPUpload( | ||||||
|  |           $_FILES['header'], | ||||||
|  |           array( | ||||||
|  |             'authorPHID' => $viewer->getPHID(), | ||||||
|  |             'canCDN' => true, | ||||||
|  |           )); | ||||||
|  |       } else if (!$delete_header) { | ||||||
|  |         $e_file = pht('Required'); | ||||||
|  |         $errors[] = pht( | ||||||
|  |           'You must choose a file when uploading a new blog header.'); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (!$errors && !$delete_header) { | ||||||
|  |         if (!$file->isTransformableImage()) { | ||||||
|  |           $e_file = pht('Not Supported'); | ||||||
|  |           $errors[] = pht( | ||||||
|  |             'This server only supports these image formats: %s.', | ||||||
|  |             implode(', ', $supported_formats)); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (!$errors) { | ||||||
|  |         if ($delete_header) { | ||||||
|  |           $blog->setHeaderImagePHID(null); | ||||||
|  |         } else { | ||||||
|  |           $blog->setHeaderImagePHID($file->getPHID()); | ||||||
|  |           $file->attachToObject($blog->getPHID()); | ||||||
|  |         } | ||||||
|  |         $blog->save(); | ||||||
|  |         return id(new AphrontRedirectResponse())->setURI($blog_uri); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     $title = pht('Edit Blog Header'); | ||||||
|  |  | ||||||
|  |     $upload_form = id(new AphrontFormView()) | ||||||
|  |       ->setUser($viewer) | ||||||
|  |       ->setEncType('multipart/form-data') | ||||||
|  |       ->appendChild( | ||||||
|  |         id(new AphrontFormFileControl()) | ||||||
|  |           ->setName('header') | ||||||
|  |           ->setLabel(pht('Upload Header')) | ||||||
|  |           ->setError($e_file) | ||||||
|  |           ->setCaption( | ||||||
|  |             pht('Supported formats: %s', implode(', ', $supported_formats)))) | ||||||
|  |       ->appendChild( | ||||||
|  |         id(new AphrontFormCheckboxControl()) | ||||||
|  |           ->setName('delete') | ||||||
|  |           ->setLabel(pht('Delete Header')) | ||||||
|  |           ->addCheckbox( | ||||||
|  |             'delete', | ||||||
|  |             1, | ||||||
|  |             null, | ||||||
|  |             null)) | ||||||
|  |       ->appendChild( | ||||||
|  |         id(new AphrontFormSubmitControl()) | ||||||
|  |           ->addCancelButton($blog_uri) | ||||||
|  |           ->setValue(pht('Upload Header'))); | ||||||
|  |  | ||||||
|  |     $upload_box = id(new PHUIObjectBoxView()) | ||||||
|  |       ->setHeaderText(pht('Upload New Header')) | ||||||
|  |       ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) | ||||||
|  |       ->setForm($upload_form); | ||||||
|  |  | ||||||
|  |     $crumbs = $this->buildApplicationCrumbs(); | ||||||
|  |     $crumbs->addTextCrumb( | ||||||
|  |       pht('Blogs'), | ||||||
|  |       $this->getApplicationURI('blog/')); | ||||||
|  |     $crumbs->addTextCrumb( | ||||||
|  |       $blog->getName(), | ||||||
|  |       $this->getApplicationURI('blog/view/'.$id)); | ||||||
|  |     $crumbs->addTextCrumb(pht('Blog Header')); | ||||||
|  |     $crumbs->setBorder(true); | ||||||
|  |  | ||||||
|  |     $header = id(new PHUIHeaderView()) | ||||||
|  |       ->setHeader(pht('Edit Blog Header')) | ||||||
|  |       ->setHeaderIcon('fa-camera'); | ||||||
|  |  | ||||||
|  |     $view = id(new PHUITwoColumnView()) | ||||||
|  |       ->setHeader($header) | ||||||
|  |       ->setFooter(array( | ||||||
|  |         $upload_box, | ||||||
|  |       )); | ||||||
|  |  | ||||||
|  |     return $this->newPage() | ||||||
|  |       ->setTitle($title) | ||||||
|  |       ->setCrumbs($crumbs) | ||||||
|  |       ->appendChild( | ||||||
|  |         array( | ||||||
|  |           $view, | ||||||
|  |       )); | ||||||
|  |  | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -14,6 +14,7 @@ final class PhameBlogManageController extends PhameBlogController { | |||||||
|       ->setViewer($viewer) |       ->setViewer($viewer) | ||||||
|       ->withIDs(array($id)) |       ->withIDs(array($id)) | ||||||
|       ->needProfileImage(true) |       ->needProfileImage(true) | ||||||
|  |       ->needHeaderImage(true) | ||||||
|       ->executeOne(); |       ->executeOne(); | ||||||
|     if (!$blog) { |     if (!$blog) { | ||||||
|       return new Aphront404Response(); |       return new Aphront404Response(); | ||||||
| @@ -40,6 +41,7 @@ final class PhameBlogManageController extends PhameBlogController { | |||||||
|  |  | ||||||
|     $curtain = $this->buildCurtain($blog); |     $curtain = $this->buildCurtain($blog); | ||||||
|     $properties = $this->buildPropertyView($blog); |     $properties = $this->buildPropertyView($blog); | ||||||
|  |     $file = $this->buildFileView($blog); | ||||||
|  |  | ||||||
|     $crumbs = $this->buildApplicationCrumbs(); |     $crumbs = $this->buildApplicationCrumbs(); | ||||||
|     $crumbs->addTextCrumb( |     $crumbs->addTextCrumb( | ||||||
| @@ -62,6 +64,7 @@ final class PhameBlogManageController extends PhameBlogController { | |||||||
|       ->setHeader($header) |       ->setHeader($header) | ||||||
|       ->setCurtain($curtain) |       ->setCurtain($curtain) | ||||||
|       ->addPropertySection(pht('Details'), $properties) |       ->addPropertySection(pht('Details'), $properties) | ||||||
|  |       ->addPropertySection(pht('Header'), $file) | ||||||
|       ->setMainColumn( |       ->setMainColumn( | ||||||
|         array( |         array( | ||||||
|           $timeline, |           $timeline, | ||||||
| @@ -157,6 +160,14 @@ final class PhameBlogManageController extends PhameBlogController { | |||||||
|         ->setDisabled(!$can_edit) |         ->setDisabled(!$can_edit) | ||||||
|         ->setWorkflow(!$can_edit)); |         ->setWorkflow(!$can_edit)); | ||||||
|  |  | ||||||
|  |     $curtain->addAction( | ||||||
|  |       id(new PhabricatorActionView()) | ||||||
|  |         ->setIcon('fa-camera') | ||||||
|  |         ->setHref($this->getApplicationURI('blog/header/'.$blog->getID().'/')) | ||||||
|  |         ->setName(pht('Edit Blog Header')) | ||||||
|  |         ->setDisabled(!$can_edit) | ||||||
|  |         ->setWorkflow(!$can_edit)); | ||||||
|  |  | ||||||
|     $curtain->addAction( |     $curtain->addAction( | ||||||
|       id(new PhabricatorActionView()) |       id(new PhabricatorActionView()) | ||||||
|         ->setIcon('fa-picture-o') |         ->setIcon('fa-picture-o') | ||||||
| @@ -188,4 +199,24 @@ final class PhameBlogManageController extends PhameBlogController { | |||||||
|     return $curtain; |     return $curtain; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private function buildFileView( | ||||||
|  |     PhameBlog $blog) { | ||||||
|  |     $viewer = $this->getViewer(); | ||||||
|  |  | ||||||
|  |     $view = id(new PHUIPropertyListView()) | ||||||
|  |       ->setUser($viewer); | ||||||
|  |  | ||||||
|  |     if ($blog->getHeaderImagePHID()) { | ||||||
|  |       $view->addImageContent( | ||||||
|  |         phutil_tag( | ||||||
|  |           'img', | ||||||
|  |           array( | ||||||
|  |             'src'     => $blog->getHeaderImageURI(), | ||||||
|  |             'class'   => 'phabricator-image-macro-hero', | ||||||
|  |           ))); | ||||||
|  |       return $view; | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,10 +3,6 @@ | |||||||
| final class PhameBlogProfilePictureController | final class PhameBlogProfilePictureController | ||||||
|   extends PhameBlogController { |   extends PhameBlogController { | ||||||
|  |  | ||||||
|   public function shouldRequireAdmin() { |  | ||||||
|     return false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public function handleRequest(AphrontRequest $request) { |   public function handleRequest(AphrontRequest $request) { | ||||||
|     $viewer = $request->getViewer(); |     $viewer = $request->getViewer(); | ||||||
|     $id = $request->getURIData('id'); |     $id = $request->getURIData('id'); | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ final class PhameBlogQuery extends PhabricatorCursorPagedPolicyAwareQuery { | |||||||
|  |  | ||||||
|   private $needBloggers; |   private $needBloggers; | ||||||
|   private $needProfileImage; |   private $needProfileImage; | ||||||
|  |   private $needHeaderImage; | ||||||
|  |  | ||||||
|   public function withIDs(array $ids) { |   public function withIDs(array $ids) { | ||||||
|     $this->ids = $ids; |     $this->ids = $ids; | ||||||
| @@ -35,6 +36,11 @@ final class PhameBlogQuery extends PhabricatorCursorPagedPolicyAwareQuery { | |||||||
|     return $this; |     return $this; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function needHeaderImage($need) { | ||||||
|  |     $this->needHeaderImage = $need; | ||||||
|  |     return $this; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public function newResultObject() { |   public function newResultObject() { | ||||||
|     return new PhameBlog(); |     return new PhameBlog(); | ||||||
|   } |   } | ||||||
| @@ -107,6 +113,28 @@ final class PhameBlogQuery extends PhabricatorCursorPagedPolicyAwareQuery { | |||||||
|         $blog->attachProfileImageFile($file); |         $blog->attachProfileImageFile($file); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if ($this->needHeaderImage) { | ||||||
|  |       $file_phids = mpull($blogs, 'getHeaderImagePHID'); | ||||||
|  |       $file_phids = array_filter($file_phids); | ||||||
|  |       if ($file_phids) { | ||||||
|  |         $files = id(new PhabricatorFileQuery()) | ||||||
|  |           ->setParentQuery($this) | ||||||
|  |           ->setViewer($this->getViewer()) | ||||||
|  |           ->withPHIDs($file_phids) | ||||||
|  |           ->execute(); | ||||||
|  |         $files = mpull($files, null, 'getPHID'); | ||||||
|  |       } else { | ||||||
|  |         $files = array(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       foreach ($blogs as $blog) { | ||||||
|  |         $file = idx($files, $blog->getHeaderImagePHID()); | ||||||
|  |         if ($file) { | ||||||
|  |           $blog->attachHeaderImageFile($file); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|     return $blogs; |     return $blogs; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -24,8 +24,10 @@ final class PhameBlog extends PhameDAO | |||||||
|   protected $status; |   protected $status; | ||||||
|   protected $mailKey; |   protected $mailKey; | ||||||
|   protected $profileImagePHID; |   protected $profileImagePHID; | ||||||
|  |   protected $headerImagePHID; | ||||||
|  |  | ||||||
|   private $profileImageFile = self::ATTACHABLE; |   private $profileImageFile = self::ATTACHABLE; | ||||||
|  |   private $headerImageFile = self::ATTACHABLE; | ||||||
|  |  | ||||||
|   const STATUS_ACTIVE = 'active'; |   const STATUS_ACTIVE = 'active'; | ||||||
|   const STATUS_ARCHIVED = 'archived'; |   const STATUS_ARCHIVED = 'archived'; | ||||||
| @@ -43,6 +45,7 @@ final class PhameBlog extends PhameDAO | |||||||
|         'status' => 'text32', |         'status' => 'text32', | ||||||
|         'mailKey' => 'bytes20', |         'mailKey' => 'bytes20', | ||||||
|         'profileImagePHID' => 'phid?', |         'profileImagePHID' => 'phid?', | ||||||
|  |         'headerImagePHID' => 'phid?', | ||||||
|  |  | ||||||
|         // T6203/NULLABILITY |         // T6203/NULLABILITY | ||||||
|         // These policies should always be non-null. |         // These policies should always be non-null. | ||||||
| @@ -212,6 +215,19 @@ final class PhameBlog extends PhameDAO | |||||||
|     return $this->assertAttached($this->profileImageFile); |     return $this->assertAttached($this->profileImageFile); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function getHeaderImageURI() { | ||||||
|  |     return $this->getHeaderImageFile()->getBestURI(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function attachHeaderImageFile(PhabricatorFile $file) { | ||||||
|  |     $this->headerImageFile = $file; | ||||||
|  |     return $this; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function getHeaderImageFile() { | ||||||
|  |     return $this->assertAttached($this->headerImageFile); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
| /* -(  PhabricatorPolicyInterface Implementation  )-------------------------- */ | /* -(  PhabricatorPolicyInterface Implementation  )-------------------------- */ | ||||||
|  |  | ||||||
|   | |||||||
| @@ -47,6 +47,7 @@ | |||||||
|   height: 9px; |   height: 9px; | ||||||
|   border-radius: 2px; |   border-radius: 2px; | ||||||
|   margin-left: 76px; |   margin-left: 76px; | ||||||
|  |   margin-bottom: 20px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .device-desktop .phui-timeline-wedge { | .device-desktop .phui-timeline-wedge { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Chad Little
					Chad Little