diff --git a/src/applications/pholio/controller/PholioMockEditController.php b/src/applications/pholio/controller/PholioMockEditController.php index 6c0ea58d13..9187d9d61f 100644 --- a/src/applications/pholio/controller/PholioMockEditController.php +++ b/src/applications/pholio/controller/PholioMockEditController.php @@ -1,373 +1,372 @@ getViewer(); $id = $request->getURIData('id'); if ($id) { $mock = id(new PholioMockQuery()) ->setViewer($viewer) ->needImages(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($id)) ->executeOne(); if (!$mock) { return new Aphront404Response(); } $title = pht('Edit Mock: %s', $mock->getName()); $is_new = false; $mock_images = $mock->getActiveImages(); $files = mpull($mock_images, 'getFile'); $mock_images = mpull($mock_images, null, 'getFilePHID'); } else { $mock = PholioMock::initializeNewMock($viewer); $title = pht('Create Mock'); $is_new = true; $files = array(); $mock_images = array(); } if ($is_new) { $v_projects = array(); } else { $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( $mock->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $v_projects = array_reverse($v_projects); } $e_name = true; $e_images = count($mock_images) ? null : true; $errors = array(); $posted_mock_images = array(); $v_name = $mock->getName(); $v_desc = $mock->getDescription(); $v_view = $mock->getViewPolicy(); $v_edit = $mock->getEditPolicy(); $v_cc = PhabricatorSubscribersQuery::loadSubscribersForPHID( $mock->getPHID()); $v_space = $mock->getSpacePHID(); if ($request->isFormPost()) { $xactions = array(); $type_name = PholioMockNameTransaction::TRANSACTIONTYPE; $type_desc = PholioMockDescriptionTransaction::TRANSACTIONTYPE; $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; $type_cc = PhabricatorTransactions::TYPE_SUBSCRIBERS; $type_space = PhabricatorTransactions::TYPE_SPACE; $v_name = $request->getStr('name'); $v_desc = $request->getStr('description'); $v_view = $request->getStr('can_view'); $v_edit = $request->getStr('can_edit'); $v_cc = $request->getArr('cc'); $v_projects = $request->getArr('projects'); $v_space = $request->getStr('spacePHID'); $mock_xactions = array(); $mock_xactions[$type_name] = $v_name; $mock_xactions[$type_desc] = $v_desc; $mock_xactions[$type_view] = $v_view; $mock_xactions[$type_edit] = $v_edit; $mock_xactions[$type_cc] = array('=' => $v_cc); $mock_xactions[$type_space] = $v_space; $file_phids = $request->getArr('file_phids'); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); $files = array_select_keys($files, $file_phids); } else { $files = array(); } if (!$files) { $e_images = pht('Required'); $errors[] = pht('You must add at least one image to the mock.'); } else { $mock->setCoverPHID(head($files)->getPHID()); } foreach ($mock_xactions as $type => $value) { $xactions[$type] = id(new PholioTransaction()) ->setTransactionType($type) ->setNewValue($value); } $order = $request->getStrList('imageOrder'); $sequence_map = array_flip($order); $replaces = $request->getArr('replaces'); $replaces_map = array_flip($replaces); /** * Foreach file posted, check to see whether we are replacing an image, * adding an image, or simply updating image metadata. Create * transactions for these cases as appropos. */ foreach ($files as $file_phid => $file) { $replaces_image_phid = null; if (isset($replaces_map[$file_phid])) { $old_file_phid = $replaces_map[$file_phid]; if ($old_file_phid != $file_phid) { $old_image = idx($mock_images, $old_file_phid); if ($old_image) { $replaces_image_phid = $old_image->getPHID(); } } } $existing_image = idx($mock_images, $file_phid); $title = (string)$request->getStr('title_'.$file_phid); $description = (string)$request->getStr('description_'.$file_phid); $sequence = $sequence_map[$file_phid]; if ($replaces_image_phid) { $replace_image = PholioImage::initializeNewImage() ->setAuthorPHID($viewer->getPHID()) ->setReplacesImagePHID($replaces_image_phid) - ->setFilePhid($file_phid) + ->setFilePHID($file_phid) ->attachFile($file) ->setName(strlen($title) ? $title : $file->getName()) ->setDescription($description) - ->setSequence($sequence); + ->setSequence($sequence) + ->save(); + $xactions[] = id(new PholioTransaction()) - ->setTransactionType( - PholioImageReplaceTransaction::TRANSACTIONTYPE) - ->setNewValue($replace_image); + ->setTransactionType(PholioImageReplaceTransaction::TRANSACTIONTYPE) + ->setNewValue($replace_image->getPHID()); + $posted_mock_images[] = $replace_image; } else if (!$existing_image) { // this is an add $add_image = PholioImage::initializeNewImage() ->setAuthorPHID($viewer->getPHID()) - ->setFilePhid($file_phid) + ->setFilePHID($file_phid) ->attachFile($file) ->setName(strlen($title) ? $title : $file->getName()) ->setDescription($description) ->setSequence($sequence); $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioImageFileTransaction::TRANSACTIONTYPE) ->setNewValue( array('+' => array($add_image))); $posted_mock_images[] = $add_image; } else { $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioImageNameTransaction::TRANSACTIONTYPE) ->setNewValue( array($existing_image->getPHID() => $title)); $xactions[] = id(new PholioTransaction()) ->setTransactionType( PholioImageDescriptionTransaction::TRANSACTIONTYPE) ->setNewValue( array($existing_image->getPHID() => $description)); $xactions[] = id(new PholioTransaction()) ->setTransactionType( PholioImageSequenceTransaction::TRANSACTIONTYPE) ->setNewValue( array($existing_image->getPHID() => $sequence)); $posted_mock_images[] = $existing_image; } } foreach ($mock_images as $file_phid => $mock_image) { if (!isset($files[$file_phid]) && !isset($replaces[$file_phid])) { // this is an outright delete $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioImageFileTransaction::TRANSACTIONTYPE) ->setNewValue( array('-' => array($mock_image))); } } if (!$errors) { $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new PholioTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $proj_edge_type) ->setNewValue(array('=' => array_fuse($v_projects))); - $mock->openTransaction(); $editor = id(new PholioMockEditor()) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setActor($viewer); $xactions = $editor->applyTransactions($mock, $xactions); - $mock->saveTransaction(); - return id(new AphrontRedirectResponse()) ->setURI('/M'.$mock->getID()); } } if ($id) { $submit = id(new AphrontFormSubmitControl()) ->addCancelButton('/M'.$id) ->setValue(pht('Save')); } else { $submit = id(new AphrontFormSubmitControl()) ->addCancelButton($this->getApplicationURI()) ->setValue(pht('Create')); } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($mock) ->execute(); // NOTE: Make this show up correctly on the rendered form. $mock->setViewPolicy($v_view); $mock->setEditPolicy($v_edit); $image_elements = array(); if ($posted_mock_images) { $display_mock_images = $posted_mock_images; } else { $display_mock_images = $mock_images; } foreach ($display_mock_images as $mock_image) { $image_elements[] = id(new PholioUploadedImageView()) ->setUser($viewer) ->setImage($mock_image) ->setReplacesPHID($mock_image->getFilePHID()); } $list_id = celerity_generate_unique_node_id(); $drop_id = celerity_generate_unique_node_id(); $order_id = celerity_generate_unique_node_id(); $list_control = phutil_tag( 'div', array( 'id' => $list_id, 'class' => 'pholio-edit-list', ), $image_elements); $drop_control = phutil_tag( 'a', array( 'id' => $drop_id, 'class' => 'pholio-edit-drop', ), pht('Click here, or drag and drop images to add them to the mock.')); $order_control = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'imageOrder', 'id' => $order_id, )); Javelin::initBehavior( 'pholio-mock-edit', array( 'listID' => $list_id, 'dropID' => $drop_id, 'orderID' => $order_id, 'uploadURI' => '/file/dropupload/', 'renderURI' => $this->getApplicationURI('image/upload/'), 'pht' => array( 'uploading' => pht('Uploading Image...'), 'uploaded' => pht('Upload Complete...'), 'undo' => pht('Undo'), 'removed' => pht('This image will be removed from the mock.'), ), )); require_celerity_resource('pholio-edit-css'); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild($order_control) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setValue($v_name) ->setLabel(pht('Name')) ->setError($e_name)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setName('description') ->setValue($v_desc) ->setLabel(pht('Description')) ->setUser($viewer)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Tags')) ->setName('projects') ->setValue($v_projects) ->setDatasource(new PhabricatorProjectDatasource())) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Subscribers')) ->setName('cc') ->setValue($v_cc) ->setUser($viewer) ->setDatasource(new PhabricatorMetaMTAMailableDatasource())) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($mock) ->setPolicies($policies) ->setSpacePHID($v_space) ->setName('can_view')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($mock) ->setPolicies($policies) ->setName('can_edit')) ->appendChild( id(new AphrontFormMarkupControl()) ->setValue($list_control)) ->appendChild( id(new AphrontFormMarkupControl()) ->setValue($drop_control) ->setError($e_images)) ->appendChild($submit); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); if (!$is_new) { $crumbs->addTextCrumb($mock->getMonogram(), '/'.$mock->getMonogram()); } $crumbs->addTextCrumb($title); $crumbs->setBorder(true); $view = id(new PHUITwoColumnView()) ->setFooter($form_box); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->addQuicksandConfig( array('mockEditConfig' => true)) ->appendChild($view); } } diff --git a/src/applications/pholio/editor/PholioMockEditor.php b/src/applications/pholio/editor/PholioMockEditor.php index a653272fba..4c0b14261b 100644 --- a/src/applications/pholio/editor/PholioMockEditor.php +++ b/src/applications/pholio/editor/PholioMockEditor.php @@ -1,278 +1,306 @@ newImages = $new_images; return $this; } public function getNewImages() { return $this->newImages; } public function getCreateObjectTitle($author, $object) { return pht('%s created this mock.', $author); } public function getCreateObjectTitleForFeed($author, $object) { return pht('%s created %s.', $author, $object); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_EDGE; $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; return $types; } protected function shouldApplyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PholioImageFileTransaction::TRANSACTIONTYPE: - case PholioImageReplaceTransaction::TRANSACTIONTYPE: return true; - break; } } return false; } protected function applyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { $new_images = array(); foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PholioImageFileTransaction::TRANSACTIONTYPE: $new_value = $xaction->getNewValue(); foreach ($new_value as $key => $txn_images) { if ($key != '+') { continue; } foreach ($txn_images as $image) { $image->save(); $new_images[] = $image; } } break; - case PholioImageReplaceTransaction::TRANSACTIONTYPE: - $image = $xaction->getNewValue(); - $image->save(); - $new_images[] = $image; - break; } } $this->setNewImages($new_images); } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { $images = $this->getNewImages(); foreach ($images as $image) { $image->setMockPHID($object->getPHID()); $image->save(); } return $xactions; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new PholioReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $name = $object->getName(); return id(new PhabricatorMetaMTAMail()) ->setSubject("M{$id}: {$name}"); } protected function getMailTo(PhabricatorLiskDAO $object) { return array( $object->getAuthorPHID(), $this->requireActor()->getPHID(), ); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $viewer = $this->requireActor(); $body = id(new PhabricatorMetaMTAMailBody()) ->setViewer($viewer); $mock_uri = $object->getURI(); $mock_uri = PhabricatorEnv::getProductionURI($mock_uri); $this->addHeadersAndCommentsToMailBody( $body, $xactions, pht('View Mock'), $mock_uri); $type_inline = PholioMockInlineTransaction::TRANSACTIONTYPE; $inlines = array(); foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $type_inline) { $inlines[] = $xaction; } } $this->appendInlineCommentsForMail($object, $inlines, $body); $body->addLinkSection( pht('MOCK DETAIL'), PhabricatorEnv::getProductionURI('/M'.$object->getID())); return $body; } private function appendInlineCommentsForMail( $object, array $inlines, PhabricatorMetaMTAMailBody $body) { if (!$inlines) { return; } $viewer = $this->requireActor(); $header = pht('INLINE COMMENTS'); $body->addRawPlaintextSection($header); $body->addRawHTMLSection(phutil_tag('strong', array(), $header)); $image_ids = array(); foreach ($inlines as $inline) { $comment = $inline->getComment(); $image_id = $comment->getImageID(); $image_ids[$image_id] = $image_id; } $images = id(new PholioImageQuery()) ->setViewer($viewer) ->withIDs($image_ids) ->execute(); $images = mpull($images, null, 'getID'); foreach ($inlines as $inline) { $comment = $inline->getComment(); $content = $comment->getContent(); $image_id = $comment->getImageID(); $image = idx($images, $image_id); if ($image) { $image_name = $image->getName(); } else { $image_name = pht('Unknown (ID %d)', $image_id); } $body->addRemarkupSection( pht('Image "%s":', $image_name), $content); } } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.pholio.subject-prefix'); } public function getMailTagsMap() { return array( PholioTransaction::MAILTAG_STATUS => pht("A mock's status changes."), PholioTransaction::MAILTAG_COMMENT => pht('Someone comments on a mock.'), PholioTransaction::MAILTAG_UPDATED => pht('Mock images or descriptions change.'), PholioTransaction::MAILTAG_OTHER => pht('Other mock activity not listed above occurs.'), ); } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function supportsSearch() { return true; } protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { return id(new HeraldPholioMockAdapter()) ->setMock($object); } protected function sortTransactions(array $xactions) { $head = array(); $tail = array(); // Move inline comments to the end, so the comments precede them. foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); if ($type == PholioMockInlineTransaction::TRANSACTIONTYPE) { $tail[] = $xaction; } else { $head[] = $xaction; } } return array_values(array_merge($head, $tail)); } protected function shouldImplyCC( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PholioMockInlineTransaction::TRANSACTIONTYPE: return true; } return parent::shouldImplyCC($object, $xaction); } + public function loadPholioImage($object, $phid) { + if (!isset($this->images[$phid])) { + + $image = id(new PholioImageQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($phid)) + ->executeOne(); + + if (!$image) { + throw new Exception( + pht( + 'No image exists with PHID "%s".', + $phid)); + } + + $mock_phid = $image->getMockPHID(); + if ($mock_phid) { + if ($mock_phid !== $object->getPHID()) { + throw new Exception( + pht( + 'Image ("%s") belongs to the wrong object ("%s", expected "%s").', + $phid, + $mock_phid, + $object->getPHID())); + } + } + + $this->images[$phid] = $image; + } + + return $this->images[$phid]; + } + } diff --git a/src/applications/pholio/xaction/PholioImageReplaceTransaction.php b/src/applications/pholio/xaction/PholioImageReplaceTransaction.php index 4978fa9768..a0e62bc1e4 100644 --- a/src/applications/pholio/xaction/PholioImageReplaceTransaction.php +++ b/src/applications/pholio/xaction/PholioImageReplaceTransaction.php @@ -1,80 +1,140 @@ getNewValue(); - return $new_image->getReplacesImagePHID(); - } + $editor = $this->getEditor(); + $new_phid = $this->getNewValue(); - public function generateNewValue($object, $value) { - return $value->getPHID(); + return $editor->loadPholioImage($object, $new_phid) + ->getReplacesImagePHID(); } - public function applyInternalEffects($object, $value) { - $old = $this->getOldValue(); - $images = $object->getImages(); - foreach ($images as $seq => $image) { - if ($image->getPHID() == $old) { - $image->setIsObsolete(1); - $image->save(); - unset($images[$seq]); - } - } - $object->attachImages($images); + public function applyExternalEffects($object, $value) { + $editor = $this->getEditor(); + $old_phid = $this->getOldValue(); + + $old_image = $editor->loadPholioImage($object, $old_phid) + ->setIsObsolete(1) + ->save(); + + $editor->loadPholioImage($object, $value) + ->setMockPHID($object->getPHID()) + ->setSequence($old_image->getSequence()) + ->save(); } public function getTitle() { return pht( '%s replaced %s with %s.', $this->renderAuthor(), $this->renderOldHandle(), $this->renderNewHandle()); } public function getTitleForFeed() { return pht( '%s updated images of %s.', $this->renderAuthor(), $this->renderObject()); } public function getIcon() { return 'fa-picture-o'; } public function getColor() { return PhabricatorTransactions::COLOR_YELLOW; } public function mergeTransactions( $object, PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { - $u_img = $u->getNewValue(); - $v_img = $v->getNewValue(); - if ($u_img->getReplacesImagePHID() == $v_img->getReplacesImagePHID()) { + + $u_phid = $u->getOldValue(); + $v_phid = $v->getOldValue(); + + if ($u_phid === $v_phid) { return $v; } + + return null; } public function extractFilePHIDs($object, $value) { - $file_phids = array(); + $editor = $this->getEditor(); + + $file_phid = $editor->loadPholioImage($object, $value) + ->getFilePHID(); + + return array($file_phid); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $mock_phid = $object->getPHID(); $editor = $this->getEditor(); - $images = $editor->getNewImages(); - foreach ($images as $image) { - if ($image->getPHID() !== $value) { + foreach ($xactions as $xaction) { + $new_phid = $xaction->getNewValue(); + + try { + $new_image = $editor->loadPholioImage($object, $new_phid); + } catch (Exception $ex) { + $errors[] = $this->newInvalidError( + pht( + 'Unable to load replacement image ("%s"): %s', + $new_phid, + $ex->getMessage()), + $xaction); continue; } - $file_phids[] = $image->getFilePHID(); + $old_phid = $new_image->getReplacesImagePHID(); + if (!$old_phid) { + $errors[] = $this->newInvalidError( + pht( + 'Image ("%s") does not specify which image it replaces.', + $new_phid), + $xaction); + continue; + } + + try { + $old_image = $editor->loadPholioImage($object, $old_phid); + } catch (Exception $ex) { + $errors[] = $this->newInvalidError( + pht( + 'Unable to load replaced image ("%s"): %s', + $old_phid, + $ex->getMessage()), + $xaction); + continue; + } + + if ($old_image->getMockPHID() !== $mock_phid) { + $errors[] = $this->newInvalidError( + pht( + 'Replaced image ("%s") belongs to the wrong mock ("%s", expected '. + '"%s").', + $old_phid, + $old_image->getMockPHID(), + $mock_phid), + $xaction); + continue; + } + + // TODO: You shouldn't be able to replace an image if it has already + // been replaced. + } - return $file_phids; + return $errors; } }