@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator

Pholio - add concept of replacing images and primitive history view

Summary:
Now you can actually replace an image! Ref T3572. This ended up needing a wee bit of infrastructure to work...

- add replace image transaction to pholio
- add replacesImagePHID to PholioImage
- tweaks to editor to properly update images with respect to replacement
- add edges to track replacement
- expose getNodes on graph query infrastructure to query the entire graph of who replaced who
- move pholio image to new phid infrastructure

Still TODO - the history view should get chopped out a bit from the current view - no more inline comments / generally less functionality plus maybe a tweak or two to make this more sensical.

Test Plan: replaced images and played with history controller a little. works okay.

Reviewers: epriestley

Reviewed By: epriestley

CC: aran, Korvin

Maniphest Tasks: T3572

Differential Revision: https://secure.phabricator.com/D6560

+544 -100
+2
resources/sql/patches/20130722.pholioreplace.sql
··· 1 + ALTER TABLE {$NAMESPACE}_pholio.pholio_image 2 + ADD COLUMN replacesImagePHID VARCHAR(64) NULL COLLATE utf8_bin;
+7
src/__phutil_library_map__.php
··· 1743 1743 'PholioController' => 'applications/pholio/controller/PholioController.php', 1744 1744 'PholioDAO' => 'applications/pholio/storage/PholioDAO.php', 1745 1745 'PholioImage' => 'applications/pholio/storage/PholioImage.php', 1746 + 'PholioImageHistoryController' => 'applications/pholio/controller/PholioImageHistoryController.php', 1747 + 'PholioImageQuery' => 'applications/pholio/query/PholioImageQuery.php', 1746 1748 'PholioImageUploadController' => 'applications/pholio/controller/PholioImageUploadController.php', 1747 1749 'PholioInlineCommentEditView' => 'applications/pholio/view/PholioInlineCommentEditView.php', 1748 1750 'PholioInlineCommentSaveView' => 'applications/pholio/view/PholioInlineCommentSaveView.php', ··· 1764 1766 'PholioMockQuery' => 'applications/pholio/query/PholioMockQuery.php', 1765 1767 'PholioMockSearchEngine' => 'applications/pholio/query/PholioMockSearchEngine.php', 1766 1768 'PholioMockViewController' => 'applications/pholio/controller/PholioMockViewController.php', 1769 + 'PholioPHIDTypeImage' => 'applications/pholio/phid/PholioPHIDTypeImage.php', 1767 1770 'PholioPHIDTypeMock' => 'applications/pholio/phid/PholioPHIDTypeMock.php', 1768 1771 'PholioRemarkupRule' => 'applications/pholio/remarkup/PholioRemarkupRule.php', 1769 1772 'PholioReplyHandler' => 'applications/pholio/mail/PholioReplyHandler.php', ··· 3795 3798 array( 3796 3799 0 => 'PholioDAO', 3797 3800 1 => 'PhabricatorMarkupInterface', 3801 + 2 => 'PhabricatorPolicyInterface', 3798 3802 ), 3803 + 'PholioImageHistoryController' => 'PholioController', 3804 + 'PholioImageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 3799 3805 'PholioImageUploadController' => 'PholioController', 3800 3806 'PholioInlineCommentEditView' => 'AphrontView', 3801 3807 'PholioInlineCommentSaveView' => 'AphrontView', ··· 3829 3835 'PholioMockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 3830 3836 'PholioMockSearchEngine' => 'PhabricatorApplicationSearchEngine', 3831 3837 'PholioMockViewController' => 'PholioController', 3838 + 'PholioPHIDTypeImage' => 'PhabricatorPHIDType', 3832 3839 'PholioPHIDTypeMock' => 'PhabricatorPHIDType', 3833 3840 'PholioRemarkupRule' => 'PhabricatorRemarkupRuleObject', 3834 3841 'PholioReplyHandler' => 'PhabricatorMailReplyHandler',
-1
src/applications/phid/PhabricatorObjectHandle.php
··· 109 109 110 110 static $map = array( 111 111 PhabricatorPHIDConstants::PHID_TYPE_USER => 'User', 112 - PhabricatorPHIDConstants::PHID_TYPE_PIMG => 'Pholio Image', 113 112 PhabricatorPHIDConstants::PHID_TYPE_BLOG => 'Blog', 114 113 PhabricatorPHIDConstants::PHID_TYPE_POST => 'Post', 115 114 PhabricatorPHIDConstants::PHID_TYPE_LEGD => 'Legalpad Document',
-1
src/applications/phid/PhabricatorPHIDConstants.php
··· 17 17 const PHID_TYPE_TOBJ = 'TOBJ'; 18 18 const PHID_TYPE_BLOG = 'BLOG'; 19 19 const PHID_TYPE_ANSW = 'ANSW'; 20 - const PHID_TYPE_PIMG = 'PIMG'; 21 20 const PHID_TYPE_CONP = 'CONP'; 22 21 const PHID_TYPE_ACNT = 'ACNT'; 23 22 const PHID_TYPE_PDCT = 'PDCT';
-24
src/applications/phid/handle/PhabricatorObjectHandleData.php
··· 68 68 $phids); 69 69 return mpull($projects, null, 'getPHID'); 70 70 71 - case PhabricatorPHIDConstants::PHID_TYPE_PIMG: 72 - $images = id(new PholioImage()) 73 - ->loadAllWhere('phid IN (%Ls)', $phids); 74 - return mpull($images, null, 'getPHID'); 75 - 76 71 case PhabricatorPHIDConstants::PHID_TYPE_XACT: 77 72 $subtypes = array(); 78 73 foreach ($phids as $phid) { ··· 295 290 $handle->setName($post->getTitle()); 296 291 $handle->setFullName($post->getTitle()); 297 292 $handle->setURI('/phame/post/view/'.$post->getID().'/'); 298 - $handle->setComplete(true); 299 - } 300 - $handles[$phid] = $handle; 301 - } 302 - break; 303 - 304 - case PhabricatorPHIDConstants::PHID_TYPE_PIMG: 305 - foreach ($phids as $phid) { 306 - $handle = new PhabricatorObjectHandle(); 307 - $handle->setPHID($phid); 308 - $handle->setType($type); 309 - if (empty($objects[$phid])) { 310 - $handle->setName('Unknown Image'); 311 - } else { 312 - $image = $objects[$phid]; 313 - $handle->setName($image->getName()); 314 - $handle->setFullName($image->getName()); 315 - $handle->setURI( 316 - '/M'.$image->getMockID().'/'.$image->getID().'/'); 317 293 $handle->setComplete(true); 318 294 } 319 295 $handles[$phid] = $handle;
+1
src/applications/pholio/application/PhabricatorApplicationPholio.php
··· 58 58 ), 59 59 'image/' => array( 60 60 'upload/' => 'PholioImageUploadController', 61 + 'history/(?P<id>\d+)/' => 'PholioImageHistoryController', 61 62 ), 62 63 ), 63 64 );
+1
src/applications/pholio/constants/PholioTransactionType.php
··· 13 13 const TYPE_IMAGE_FILE = 'image-file'; 14 14 const TYPE_IMAGE_NAME= 'image-name'; 15 15 const TYPE_IMAGE_DESCRIPTION = 'image-description'; 16 + const TYPE_IMAGE_REPLACE = 'image-replace'; 16 17 17 18 /* your witty commentary at the mock : image : x,y level */ 18 19 const TYPE_INLINE = 'inline';
+96
src/applications/pholio/controller/PholioImageHistoryController.php
··· 1 + <?php 2 + 3 + /** 4 + * @group pholio 5 + */ 6 + final class PholioImageHistoryController extends PholioController { 7 + 8 + private $imageID; 9 + 10 + public function willProcessRequest(array $data) { 11 + $this->imageID = $data['id']; 12 + } 13 + 14 + public function processRequest() { 15 + $request = $this->getRequest(); 16 + $user = $request->getUser(); 17 + 18 + $image = id(new PholioImageQuery()) 19 + ->setViewer($user) 20 + ->withIDs(array($this->imageID)) 21 + ->executeOne(); 22 + 23 + if (!$image) { 24 + return new Aphront404Response(); 25 + } 26 + 27 + // note while we have a mock object, its missing images we need to show 28 + // the history of what's happened here. 29 + // fetch the real deal 30 + // 31 + $mock = id(new PholioMockQuery()) 32 + ->setViewer($user) 33 + ->needImages(true) 34 + ->withIDs(array($image->getMockID())) 35 + ->executeOne(); 36 + 37 + $phids = array($mock->getAuthorPHID()); 38 + $this->loadHandles($phids); 39 + 40 + $engine = id(new PhabricatorMarkupEngine()) 41 + ->setViewer($user); 42 + $engine->addObject($mock, PholioMock::MARKUP_FIELD_DESCRIPTION); 43 + $engine->process(); 44 + 45 + 46 + $images = $mock->getImageHistorySet($this->imageID); 47 + // TODO - this is a hack until we specialize the view object 48 + $mock->attachImages($images); 49 + $latest_image = last($images); 50 + 51 + $title = pht( 52 + 'Image history for "%s" from the mock "%s."', 53 + $latest_image->getName(), 54 + $mock->getName()); 55 + 56 + $header = id(new PhabricatorHeaderView()) 57 + ->setHeader($title); 58 + 59 + require_celerity_resource('pholio-css'); 60 + require_celerity_resource('pholio-inline-comments-css'); 61 + 62 + $comment_form_id = celerity_generate_unique_node_id(); 63 + $output = id(new PholioMockImagesView()) 64 + ->setRequestURI($request->getRequestURI()) 65 + ->setCommentFormID($comment_form_id) 66 + ->setUser($user) 67 + ->setMock($mock) 68 + ->setImageID($this->imageID); 69 + 70 + $crumbs = $this->buildApplicationCrumbs(); 71 + $crumbs 72 + ->addCrumb( 73 + id(new PhabricatorCrumbView()) 74 + ->setName('M'.$mock->getID()) 75 + ->setHref('/M'.$mock->getID())) 76 + ->addCrumb( 77 + id(new PhabricatorCrumbView()) 78 + ->setName('Image History') 79 + ->setHref($request->getRequestURI())); 80 + 81 + $content = array( 82 + $crumbs, 83 + $header, 84 + $output->render(), 85 + ); 86 + 87 + return $this->buildApplicationPage( 88 + $content, 89 + array( 90 + 'title' => 'M'.$mock->getID().' '.$title, 91 + 'device' => true, 92 + 'pageObjects' => array($mock->getPHID()), 93 + )); 94 + } 95 + 96 + }
+38 -9
src/applications/pholio/controller/PholioMockEditController.php
··· 111 111 } 112 112 113 113 $sequence = 0; 114 + $replaces = $request->getArr('replaces'); 115 + $replaces_map = array_flip($replaces); 116 + 117 + /** 118 + * Foreach file posted, check to see whether we are replacing an image, 119 + * adding an image, or simply updating image metadata. Create 120 + * transactions for these cases as appropos. 121 + */ 114 122 foreach ($files as $file_phid => $file) { 115 - $mock_image = idx($mock_images, $file_phid); 123 + $replaces_image_phid = null; 124 + if (isset($replaces_map[$file_phid])) { 125 + $old_file_phid = $replaces_map[$file_phid]; 126 + $old_image = idx($mock_images, $old_file_phid); 127 + if ($old_image) { 128 + $replaces_image_phid = $old_image->getPHID(); 129 + } 130 + } 131 + 132 + $existing_image = idx($mock_images, $file_phid); 133 + 116 134 $title = (string)$request->getStr('title_'.$file_phid); 117 135 $description = (string)$request->getStr('description_'.$file_phid); 118 - if (!$mock_image) { 119 - // this is an add 136 + 137 + if ($replaces_image_phid) { 138 + $replace_image = id(new PholioImage()) 139 + ->setReplacesImagePHID($replaces_image_phid) 140 + ->setFilePhid($file_phid) 141 + ->setName(strlen($title) ? $title : $file->getName()) 142 + ->setDescription($description) 143 + ->setSequence($sequence); 144 + $xactions[] = id(new PholioTransaction()) 145 + ->setTransactionType( 146 + PholioTransactionType::TYPE_IMAGE_REPLACE) 147 + ->setNewValue($replace_image); 148 + } else if (!$existing_image) { // this is an add 120 149 $add_image = id(new PholioImage()) 121 150 ->setFilePhid($file_phid) 122 151 ->setName(strlen($title) ? $title : $file->getName()) ··· 127 156 ->setNewValue( 128 157 array('+' => array($add_image))); 129 158 } else { 130 - // update (maybe) 131 159 $xactions[] = id(new PholioTransaction()) 132 160 ->setTransactionType(PholioTransactionType::TYPE_IMAGE_NAME) 133 161 ->setNewValue( 134 - array($mock_image->getPHID() => $title)); 162 + array($existing_image->getPHID() => $title)); 135 163 $xactions[] = id(new PholioTransaction()) 136 164 ->setTransactionType( 137 165 PholioTransactionType::TYPE_IMAGE_DESCRIPTION) 138 - ->setNewValue(array($mock_image->getPHID() => $description)); 139 - $mock_image->setSequence($sequence); 166 + ->setNewValue( 167 + array($existing_image->getPHID() => $description)); 168 + $existing_image->setSequence($sequence); 140 169 } 141 170 $sequence++; 142 171 } 143 172 foreach ($mock_images as $file_phid => $mock_image) { 144 - if (!isset($files[$file_phid])) { 145 - // this is a delete 173 + if (!isset($files[$file_phid]) && !isset($replaces[$file_phid])) { 174 + // this is an outright delete 146 175 $xactions[] = id(new PholioTransaction()) 147 176 ->setTransactionType(PholioTransactionType::TYPE_IMAGE_FILE) 148 177 ->setNewValue(
+40
src/applications/pholio/controller/PholioMockViewController.php
··· 76 76 require_celerity_resource('pholio-css'); 77 77 require_celerity_resource('pholio-inline-comments-css'); 78 78 79 + $image_status = $this->getImageStatus($mock, $this->imageID); 80 + 79 81 $comment_form_id = celerity_generate_unique_node_id(); 80 82 $output = id(new PholioMockImagesView()) 81 83 ->setRequestURI($request->getRequestURI()) ··· 100 102 101 103 $content = array( 102 104 $crumbs, 105 + $image_status, 103 106 $header, 104 107 $actions, 105 108 $properties, ··· 115 118 'device' => true, 116 119 'pageObjects' => array($mock->getPHID()), 117 120 )); 121 + } 122 + 123 + private function getImageStatus(PholioMock $mock, $image_id) { 124 + $status = null; 125 + $images = $mock->getImages(); 126 + foreach ($images as $image) { 127 + if ($image->getID() == $image_id) { 128 + return $status; 129 + } 130 + } 131 + 132 + $images = $mock->getAllImages(); 133 + $images = mpull($images, null, 'getID'); 134 + $image = idx($images, $image_id); 135 + 136 + if ($image) { 137 + $history = $mock->getImageUpdateSet($image_id); 138 + $latest_image = last($history); 139 + $href = $this->getApplicationURI( 140 + 'image/history/'.$latest_image->getID().'/'); 141 + $status = id(new AphrontErrorView()) 142 + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) 143 + ->setTitle(pht('The requested image is obsolete.')) 144 + ->appendChild(phutil_tag( 145 + 'p', 146 + array(), 147 + array( 148 + pht('You are viewing this mock with the latest image set.'), 149 + ' ', 150 + phutil_tag( 151 + 'a', 152 + array('href' => $href), 153 + pht( 154 + 'Click here to see the history of the now obsolete image.'))))); 155 + } 156 + 157 + return $status; 118 158 } 119 159 120 160 private function buildActionView(PholioMock $mock) {
+32 -3
src/applications/pholio/editor/PholioMockEditor.php
··· 30 30 $types[] = PholioTransactionType::TYPE_IMAGE_FILE; 31 31 $types[] = PholioTransactionType::TYPE_IMAGE_NAME; 32 32 $types[] = PholioTransactionType::TYPE_IMAGE_DESCRIPTION; 33 + $types[] = PholioTransactionType::TYPE_IMAGE_REPLACE; 33 34 34 35 return $types; 35 36 } ··· 64 65 $phid = $image->getPHID(); 65 66 } 66 67 return array($phid => $description); 68 + case PholioTransactionType::TYPE_IMAGE_REPLACE: 69 + $raw = $xaction->getNewValue(); 70 + return $raw->getReplacesImagePHID(); 67 71 } 68 72 } 69 73 ··· 77 81 case PholioTransactionType::TYPE_IMAGE_NAME: 78 82 case PholioTransactionType::TYPE_IMAGE_DESCRIPTION: 79 83 return $xaction->getNewValue(); 84 + case PholioTransactionType::TYPE_IMAGE_REPLACE: 85 + $raw = $xaction->getNewValue(); 86 + return $raw->getPHID(); 80 87 case PholioTransactionType::TYPE_IMAGE_FILE: 81 88 $raw_new_value = $xaction->getNewValue(); 82 89 $new_value = array(); ··· 107 114 foreach ($xactions as $xaction) { 108 115 switch ($xaction->getTransactionType()) { 109 116 case PholioTransactionType::TYPE_IMAGE_FILE: 117 + case PholioTransactionType::TYPE_IMAGE_REPLACE: 110 118 return true; 111 119 break; 112 120 } ··· 133 141 } 134 142 } 135 143 break; 144 + case PholioTransactionType::TYPE_IMAGE_REPLACE: 145 + $image = $xaction->getNewValue(); 146 + $image->save(); 147 + $new_images[] = $image; 148 + break; 136 149 } 137 150 } 138 151 $this->setNewImages($new_images); ··· 189 202 } 190 203 $object->attachImages($images); 191 204 break; 192 - case PholioTransactionType::TYPE_IMAGE_NAME: 193 - $image = $this->getImageForXaction($object, $xaction); 194 - $value = (string) head($xaction->getNewValue()); 205 + case PholioTransactionType::TYPE_IMAGE_REPLACE: 206 + $old = $xaction->getOldValue(); 207 + $images = $object->getImages(); 208 + foreach ($images as $seq => $image) { 209 + if ($image->getPHID() == $old) { 210 + $image->setIsObsolete(1); 211 + $image->save(); 212 + unset($images[$seq]); 213 + } 214 + } 215 + $object->attachImages($images); 216 + break; 217 + case PholioTransactionType::TYPE_IMAGE_NAME: 218 + $image = $this->getImageForXaction($object, $xaction); 219 + $value = (string) head($xaction->getNewValue()); 195 220 $image->setName($value); 196 221 $image->save(); 197 222 break; ··· 224 249 case PholioTransactionType::TYPE_NAME: 225 250 case PholioTransactionType::TYPE_DESCRIPTION: 226 251 return $v; 252 + case PholioTransactionType::TYPE_IMAGE_REPLACE: 253 + if ($u->getNewValue() == $v->getOldValue()) { 254 + return $v; 255 + } 227 256 case PholioTransactionType::TYPE_IMAGE_FILE: 228 257 return $this->mergePHIDOrEdgeTransactions($u, $v); 229 258 case PholioTransactionType::TYPE_IMAGE_NAME:
+1
src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php
··· 42 42 $image = new PholioImage(); 43 43 $image->setFilePHID($file->getPHID()); 44 44 $image->setSequence($sequence++); 45 + $image->attachMock($mock); 45 46 $images[] = $image; 46 47 } 47 48
+47
src/applications/pholio/phid/PholioPHIDTypeImage.php
··· 1 + <?php 2 + 3 + final class PholioPHIDTypeImage extends PhabricatorPHIDType { 4 + 5 + const TYPECONST = 'PIMG'; 6 + 7 + public function getTypeConstant() { 8 + return self::TYPECONST; 9 + } 10 + 11 + public function getTypeName() { 12 + return pht('Image'); 13 + } 14 + 15 + public function newObject() { 16 + return new PholioImage(); 17 + } 18 + 19 + public function loadObjects( 20 + PhabricatorObjectQuery $query, 21 + array $phids) { 22 + 23 + return id(new PholioImageQuery()) 24 + ->setViewer($query->getViewer()) 25 + ->withPHIDs($phids) 26 + ->execute(); 27 + } 28 + 29 + public function loadHandles( 30 + PhabricatorHandleQuery $query, 31 + array $handles, 32 + array $objects) { 33 + 34 + foreach ($handles as $phid => $handle) { 35 + $image = $objects[$phid]; 36 + 37 + $id = $image->getID(); 38 + $mock_id = $image->getMockID(); 39 + $name = $image->getName(); 40 + 41 + $handle->setURI("/M{$mock_id}/{$id}/"); 42 + $handle->setName($name); 43 + $handle->setFullName($name); 44 + } 45 + } 46 + 47 + }
+148
src/applications/pholio/query/PholioImageQuery.php
··· 1 + <?php 2 + 3 + /** 4 + * @group pholio 5 + */ 6 + final class PholioImageQuery 7 + extends PhabricatorCursorPagedPolicyAwareQuery { 8 + 9 + private $ids; 10 + private $phids; 11 + private $mockIDs; 12 + private $obsolete; 13 + 14 + private $needInlineComments; 15 + private $mockCache = array(); 16 + 17 + public function withIDs(array $ids) { 18 + $this->ids = $ids; 19 + return $this; 20 + } 21 + 22 + public function withPHIDs(array $phids) { 23 + $this->phids = $phids; 24 + return $this; 25 + } 26 + 27 + public function withMockIDs(array $mock_ids) { 28 + $this->mockIDs = $mock_ids; 29 + return $this; 30 + } 31 + 32 + public function withObsolete($obsolete) { 33 + $this->obsolete = $obsolete; 34 + return $this; 35 + } 36 + 37 + public function needInlineComments($need_inline_comments) { 38 + $this->needInlineComments = $need_inline_comments; 39 + return $this; 40 + } 41 + 42 + public function setMockCache($mock_cache) { 43 + $this->mockCache = $mock_cache; 44 + return $this; 45 + } 46 + public function getMockCache() { 47 + return $this->mockCache; 48 + } 49 + 50 + protected function loadPage() { 51 + $table = new PholioImage(); 52 + $conn_r = $table->establishConnection('r'); 53 + 54 + $data = queryfx_all( 55 + $conn_r, 56 + 'SELECT * FROM %T %Q %Q %Q', 57 + $table->getTableName(), 58 + $this->buildWhereClause($conn_r), 59 + $this->buildOrderClause($conn_r), 60 + $this->buildLimitClause($conn_r)); 61 + 62 + $images = $table->loadAllFromArray($data); 63 + 64 + return $images; 65 + } 66 + 67 + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { 68 + $where = array(); 69 + 70 + $where[] = $this->buildPagingClause($conn_r); 71 + 72 + if ($this->ids) { 73 + $where[] = qsprintf( 74 + $conn_r, 75 + 'id IN (%Ld)', 76 + $this->ids); 77 + } 78 + 79 + if ($this->phids) { 80 + $where[] = qsprintf( 81 + $conn_r, 82 + 'phid IN (%Ls)', 83 + $this->phids); 84 + } 85 + 86 + if ($this->mockIDs) { 87 + $where[] = qsprintf( 88 + $conn_r, 89 + 'mockID IN (%Ld)', 90 + $this->mockIDs); 91 + } 92 + 93 + if ($this->obsolete !== null) { 94 + $where[] = qsprintf( 95 + $conn_r, 96 + 'isObsolete = %d', 97 + $this->obsolete); 98 + } 99 + 100 + return $this->formatWhereClause($where); 101 + } 102 + 103 + protected function willFilterPage(array $images) { 104 + assert_instances_of($images, 'PholioImage'); 105 + 106 + $file_phids = mpull($images, 'getFilePHID'); 107 + $all_files = mpull(id(new PhabricatorFile())->loadAllWhere( 108 + 'phid IN (%Ls)', 109 + $file_phids), null, 'getPHID'); 110 + 111 + if ($this->needInlineComments) { 112 + $all_inline_comments = id(new PholioTransactionComment()) 113 + ->loadAllWhere('imageid IN (%Ld)', 114 + mpull($images, 'getID')); 115 + $all_inline_comments = mgroup($all_inline_comments, 'getImageID'); 116 + } 117 + 118 + foreach ($images as $image) { 119 + $file = idx($all_files, $image->getFilePHID()); 120 + if (!$file) { 121 + $file = PhabricatorFile::loadBuiltin($this->getViewer(), 'missing.png'); 122 + } 123 + $image->attachFile($file); 124 + if ($this->needInlineComments) { 125 + $inlines = idx($all_inline_comments, $image->getID(), array()); 126 + $image->attachInlineComments($inlines); 127 + } 128 + } 129 + 130 + if ($this->getMockCache()) { 131 + $mocks = $this->getMockCache(); 132 + } else { 133 + $mock_ids = mpull($images, 'getMockID'); 134 + // DO NOT set needImages to true; recursion results! 135 + $mocks = id(new PholioMockQuery()) 136 + ->setViewer($this->getViewer()) 137 + ->withIDs($mock_ids) 138 + ->execute(); 139 + $mocks = mpull($mocks, null, 'getID'); 140 + } 141 + foreach ($images as $image) { 142 + $image->attachMock($mocks[$image->getMockID()]); 143 + } 144 + 145 + return $images; 146 + } 147 + 148 + }
+9 -30
src/applications/pholio/query/PholioMockQuery.php
··· 111 111 private function loadImages(array $mocks) { 112 112 assert_instances_of($mocks, 'PholioMock'); 113 113 114 - $mock_ids = mpull($mocks, 'getID'); 115 - $all_images = id(new PholioImage())->loadAllWhere( 116 - 'mockID IN (%Ld) AND isObsolete = %d', 117 - $mock_ids, 118 - 0); 119 - 120 - $file_phids = mpull($all_images, 'getFilePHID'); 121 - $all_files = mpull(id(new PhabricatorFile())->loadAllWhere( 122 - 'phid IN (%Ls)', 123 - $file_phids), null, 'getPHID'); 124 - 125 - if ($this->needInlineComments) { 126 - $all_inline_comments = id(new PholioTransactionComment()) 127 - ->loadAllWhere('imageid IN (%Ld)', 128 - mpull($all_images, 'getID')); 129 - $all_inline_comments = mgroup($all_inline_comments, 'getImageID'); 130 - } 131 - 132 - foreach ($all_images as $image) { 133 - $file = idx($all_files, $image->getFilePHID()); 134 - if (!$file) { 135 - $file = PhabricatorFile::loadBuiltin($this->getViewer(), 'missing.png'); 136 - } 137 - $image->attachFile($file); 138 - if ($this->needInlineComments) { 139 - $inlines = idx($all_images, $image->getID(), array()); 140 - $image->attachInlineComments($inlines); 141 - } 142 - } 114 + $mock_map = mpull($mocks, null, 'getID'); 115 + $all_images = id(new PholioImageQuery()) 116 + ->setViewer($this->getViewer()) 117 + ->setMockCache($mock_map) 118 + ->withMockIDs(array_keys($mock_map)) 119 + ->needInlineComments($this->needInlineComments) 120 + ->execute(); 143 121 144 122 $image_groups = mgroup($all_images, 'getMockID'); 145 123 146 124 foreach ($mocks as $mock) { 147 125 $mock_images = $image_groups[$mock->getID()]; 148 - $mock->attachImages($mock_images); 126 + $mock->attachAllImages($mock_images); 127 + $mock->attachImages(mfilter($mock_images, 'getIsObsolete', true)); 149 128 } 150 129 } 151 130
+41 -15
src/applications/pholio/storage/PholioImage.php
··· 4 4 * @group pholio 5 5 */ 6 6 final class PholioImage extends PholioDAO 7 - implements PhabricatorMarkupInterface { 7 + implements 8 + PhabricatorMarkupInterface, 9 + PhabricatorPolicyInterface { 8 10 9 11 const MARKUP_FIELD_DESCRIPTION = 'markup:description'; 10 12 ··· 14 16 protected $description = ''; 15 17 protected $sequence; 16 18 protected $isObsolete = 0; 19 + protected $replacesImagePHID = null; 17 20 18 - private $inlineComments; 19 - private $file; 21 + private $inlineComments = self::ATTACHABLE; 22 + private $file = self::ATTACHABLE; 23 + private $mock = self::ATTACHABLE; 20 24 21 25 public function getConfiguration() { 22 26 return array( ··· 25 29 } 26 30 27 31 public function generatePHID() { 28 - return PhabricatorPHID::generateNewPHID('PIMG'); 32 + return PhabricatorPHID::generateNewPHID(PholioPHIDTypeImage::TYPECONST); 29 33 } 30 34 35 + public function attachFile(PhabricatorFile $file) { 36 + $this->file = $file; 37 + return $this; 38 + } 39 + 40 + public function getFile() { 41 + $this->assertAttached($this->file); 42 + return $this->file; 43 + } 44 + 45 + public function attachMock(PholioMock $mock) { 46 + $this->mock = $mock; 47 + return $this; 48 + } 49 + 50 + public function getMock() { 51 + $this->assertAttached($this->mock); 52 + return $this->mock; 53 + } 54 + 55 + 31 56 public function attachInlineComments(array $inline_comments) { 32 57 assert_instances_of($inline_comments, 'PholioTransactionComment'); 33 58 $this->inlineComments = $inline_comments; ··· 35 60 } 36 61 37 62 public function getInlineComments() { 38 - if ($this->inlineComments === null) { 39 - throw new Exception("Call attachImages() before getImages()!"); 40 - } 63 + $this->assertAttached($this->inlineComments); 41 64 return $this->inlineComments; 42 65 } 43 66 ··· 66 89 return (bool)$this->getID(); 67 90 } 68 91 69 - public function attachFile(PhabricatorFile $file) { 70 - $this->file = $file; 71 - return $this; 92 + /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ 93 + 94 + public function getCapabilities() { 95 + return $this->getMock()->getCapabilities(); 96 + } 97 + 98 + public function getPolicy($capability) { 99 + return $this->getMock()->getPolicy($capability); 72 100 } 73 101 74 - public function getFile() { 75 - if ($this->file === null) { 76 - throw new Exception("Call attachFile() before getFile()!"); 77 - } 78 - return $this->file; 102 + // really the *mock* controls who can see an image 103 + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 104 + return $this->getMock()->hasAutomaticCapability($capability, $viewer); 79 105 } 80 106 81 107 }
+49 -12
src/applications/pholio/storage/PholioMock.php
··· 22 22 protected $coverPHID; 23 23 protected $mailKey; 24 24 25 - private $images; 26 - private $coverFile; 27 - private $tokenCount; 25 + private $images = self::ATTACHABLE; 26 + private $allImages = self::ATTACHABLE; 27 + private $coverFile = self::ATTACHABLE; 28 + private $tokenCount = self::ATTACHABLE; 28 29 29 30 public function getConfiguration() { 30 31 return array( ··· 43 44 return parent::save(); 44 45 } 45 46 47 + /** 48 + * These should be the images currently associated with the Mock. 49 + */ 46 50 public function attachImages(array $images) { 47 51 assert_instances_of($images, 'PholioImage'); 48 52 $this->images = $images; ··· 50 54 } 51 55 52 56 public function getImages() { 53 - if ($this->images === null) { 54 - throw new Exception("Call attachImages() before getImages()!"); 55 - } 57 + $this->assertAttached($this->images); 56 58 return $this->images; 57 59 } 58 60 61 + /** 62 + * These should be *all* images associated with the Mock. This includes 63 + * images which have been removed and / or replaced from the Mock. 64 + */ 65 + public function attachAllImages(array $images) { 66 + assert_instances_of($images, 'PholioImage'); 67 + $this->allImages = $images; 68 + return $this; 69 + } 70 + 71 + public function getAllImages() { 72 + $this->assertAttached($this->images); 73 + return $this->allImages; 74 + } 75 + 59 76 public function attachCoverFile(PhabricatorFile $file) { 60 77 $this->coverFile = $file; 61 78 return $this; 62 79 } 63 80 64 81 public function getCoverFile() { 65 - if ($this->coverFile === null) { 66 - throw new Exception("Call attachCoverFile() before getCoverFile()!"); 67 - } 82 + $this->assertAttached($this->coverFile); 68 83 return $this->coverFile; 69 84 } 70 85 71 86 public function getTokenCount() { 72 - if ($this->tokenCount === null) { 73 - throw new Exception("Call attachTokenCount() before getTokenCount()!"); 74 - } 87 + $this->assertAttached($this->tokenCount); 75 88 return $this->tokenCount; 76 89 } 77 90 78 91 public function attachTokenCount($count) { 79 92 $this->tokenCount = $count; 80 93 return $this; 94 + } 95 + 96 + public function getImageHistorySet($image_id) { 97 + $images = $this->getAllImages(); 98 + $images = mpull($images, null, 'getID'); 99 + $selected_image = $images[$image_id]; 100 + 101 + $replace_map = mpull($images, null, 'getReplacesImagePHID'); 102 + $phid_map = mpull($images, null, 'getPHID'); 103 + 104 + // find the earliest image 105 + $image = $selected_image; 106 + while (isset($phid_map[$image->getReplacesImagePHID()])) { 107 + $image = $phid_map[$image->getReplacesImagePHID()]; 108 + } 109 + 110 + // now build history moving forward 111 + $history = array($image->getID() => $image); 112 + while (isset($replace_map[$image->getPHID()])) { 113 + $image = $replace_map[$image->getPHID()]; 114 + $history[$image->getID()] = $image; 115 + } 116 + 117 + return $history; 81 118 } 82 119 83 120
+15
src/applications/pholio/storage/PholioTransaction.php
··· 36 36 case PholioTransactionType::TYPE_IMAGE_FILE: 37 37 $phids = array_merge($phids, $new, $old); 38 38 break; 39 + case PholioTransactionType::TYPE_IMAGE_REPLACE: 40 + $phids[] = $new; 41 + $phids[] = $old; 42 + break; 39 43 case PholioTransactionType::TYPE_IMAGE_DESCRIPTION: 40 44 case PholioTransactionType::TYPE_IMAGE_NAME: 41 45 $phids[] = key($new); ··· 69 73 case PholioTransactionType::TYPE_IMAGE_DESCRIPTION: 70 74 return 'edit'; 71 75 case PholioTransactionType::TYPE_IMAGE_FILE: 76 + case PholioTransactionType::TYPE_IMAGE_REPLACE: 72 77 return 'attach'; 73 78 } 74 79 ··· 115 120 $this->renderHandleLink($author_phid), 116 121 $count); 117 122 break; 123 + case PholioTransactionType::TYPE_IMAGE_REPLACE: 124 + return pht( 125 + '%s replaced %s with %s.', 126 + $this->renderHandleLink($author_phid), 127 + $this->renderHandleLink($old), 128 + $this->renderHandleLink($new)); 129 + break; 118 130 case PholioTransactionType::TYPE_IMAGE_FILE: 119 131 $add = array_diff($new, $old); 120 132 $rem = array_diff($old, $new); ··· 197 209 $this->renderHandleLink($author_phid), 198 210 $this->renderHandleLink($object_phid)); 199 211 break; 212 + case PholioTransactionType::TYPE_IMAGE_REPLACE: 200 213 case PholioTransactionType::TYPE_IMAGE_FILE: 201 214 return pht( 202 215 '%s updated images of %s.', ··· 259 272 case PholioTransactionType::TYPE_IMAGE_NAME: 260 273 case PholioTransactionType::TYPE_IMAGE_DESCRIPTION: 261 274 return PhabricatorTransactions::COLOR_BLUE; 275 + case PholioTransactionType::TYPE_IMAGE_REPLACE: 276 + return PhabricatorTransactions::COLOR_YELLOW; 262 277 case PholioTransactionType::TYPE_IMAGE_FILE: 263 278 $add = array_diff($new, $old); 264 279 $rem = array_diff($old, $new);
+6 -5
src/applications/pholio/view/PholioMockImagesView.php
··· 67 67 $y = idx($metadata, PhabricatorFile::METADATA_IMAGE_HEIGHT); 68 68 69 69 $images[] = array( 70 - 'id' => $image->getID(), 70 + 'id' => $image->getID(), 71 71 'fullURI' => $image->getFile()->getBestURI(), 72 72 'pageURI' => '/M'.$mock->getID().'/'.$image->getID().'/', 73 - 'width' => $x, 74 - 'height' => $y, 75 - 'title' => $image->getName(), 76 - 'desc' => $image->getDescription(), 73 + 'historyURI' => '/pholio/image/history/'.$image->getID().'/', 74 + 'width' => $x, 75 + 'height' => $y, 76 + 'title' => $image->getName(), 77 + 'desc' => $image->getDescription(), 77 78 ); 78 79 } 79 80
+4
src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
··· 1475 1475 'type' => 'sql', 1476 1476 'name' => $this->getPatchPath('20130723.taskstarttime.sql'), 1477 1477 ), 1478 + '20130722.pholioreplace.sql' => array( 1479 + 'type' => 'sql', 1480 + 'name' => $this->getPatchPath('20130722.pholioreplace.sql'), 1481 + ), 1478 1482 ); 1479 1483 } 1480 1484 }
+7
webroot/rsrc/js/application/pholio/behavior-pholio-mock-view.js
··· 728 728 'View Full Image'); 729 729 info.push(full_link); 730 730 731 + var history_link = JX.$N( 732 + 'a', 733 + { href: image.historyURI }, 734 + 'View Image History'); 735 + info.push(history_link); 736 + 737 + 731 738 for (var ii = 0; ii < info.length; ii++) { 732 739 info[ii] = JX.$N('div', {className: 'pholio-image-info-item'}, info[ii]); 733 740 }