diff --git a/src/applications/nuance/controller/NuanceItemEditController.php b/src/applications/nuance/controller/NuanceItemEditController.php index 89bfbda5c9..bad409a801 100644 --- a/src/applications/nuance/controller/NuanceItemEditController.php +++ b/src/applications/nuance/controller/NuanceItemEditController.php @@ -1,32 +1,93 @@ getViewer(); + $viewer = $this->getViewer(); $id = $request->getURIData('id'); - if (!$id) { - $item = new NuanceItem(); - } else { - $item = id(new NuanceItemQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->executeOne(); - } - + $item = id(new NuanceItemQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); if (!$item) { return new Aphront404Response(); } + $title = pht('Item %d', $item->getID()); + $crumbs = $this->buildApplicationCrumbs(); - $title = 'TODO'; + $crumbs->addTextCrumb($title); + $crumbs->addTextCrumb(pht('Edit')); + + $properties = $this->buildPropertyView($item); + $actions = $this->buildActionView($item); + $properties->setActionList($actions); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText($title) + ->addPropertyList($properties); return $this->buildApplicationPage( - $crumbs, + array( + $crumbs, + $box, + ), array( 'title' => $title, )); } + private function buildPropertyView(NuanceItem $item) { + $viewer = $this->getViewer(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($item); + + $properties->addProperty( + pht('Date Created'), + phabricator_datetime($item->getDateCreated(), $viewer)); + + $properties->addProperty( + pht('Requestor'), + $viewer->renderHandle($item->getRequestorPHID())); + + $properties->addProperty( + pht('Source'), + $viewer->renderHandle($item->getSourcePHID())); + + $source = $item->getSource(); + $definition = $source->requireDefinition(); + + $definition->renderItemEditProperties( + $viewer, + $item, + $properties); + + return $properties; + } + + private function buildActionView(NuanceItem $item) { + $viewer = $this->getViewer(); + $id = $item->getID(); + + $actions = id(new PhabricatorActionListView()) + ->setUser($viewer); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('View Item')) + ->setIcon('fa-eye') + ->setHref($this->getApplicationURI("item/view/{$id}/"))); + + return $actions; + } + + } diff --git a/src/applications/nuance/controller/NuanceItemViewController.php b/src/applications/nuance/controller/NuanceItemViewController.php index 9c7401cafe..d325afbd29 100644 --- a/src/applications/nuance/controller/NuanceItemViewController.php +++ b/src/applications/nuance/controller/NuanceItemViewController.php @@ -1,27 +1,86 @@ getViewer(); + $viewer = $this->getViewer(); $id = $request->getURIData('id'); $item = id(new NuanceItemQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); - if (!$item) { return new Aphront404Response(); } + $title = pht('Item %d', $item->getID()); + $crumbs = $this->buildApplicationCrumbs(); - $title = 'TODO'; + $crumbs->addTextCrumb($title); + + $properties = $this->buildPropertyView($item); + $actions = $this->buildActionView($item); + $properties->setActionList($actions); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText($title) + ->addPropertyList($properties); return $this->buildApplicationPage( - $crumbs, + array( + $crumbs, + $box, + ), array( 'title' => $title, )); } + + private function buildPropertyView(NuanceItem $item) { + $viewer = $this->getViewer(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($item); + + $properties->addProperty( + pht('Date Created'), + phabricator_datetime($item->getDateCreated(), $viewer)); + + $source = $item->getSource(); + $definition = $source->requireDefinition(); + + $definition->renderItemViewProperties( + $viewer, + $item, + $properties); + + return $properties; + } + + private function buildActionView(NuanceItem $item) { + $viewer = $this->getViewer(); + $id = $item->getID(); + + $actions = id(new PhabricatorActionListView()) + ->setUser($viewer); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $item, + PhabricatorPolicyCapability::CAN_EDIT); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Item')) + ->setIcon('fa-pencil') + ->setHref($this->getApplicationURI("item/edit/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $actions; + } + + } diff --git a/src/applications/nuance/phid/NuanceRequestorPHIDType.php b/src/applications/nuance/phid/NuanceRequestorPHIDType.php index 0e08322a80..2cf06bbb10 100644 --- a/src/applications/nuance/phid/NuanceRequestorPHIDType.php +++ b/src/applications/nuance/phid/NuanceRequestorPHIDType.php @@ -1,37 +1,39 @@ withPHIDs($phids); } public function loadHandles( PhabricatorHandleQuery $query, array $handles, array $objects) { $viewer = $query->getViewer(); foreach ($handles as $phid => $handle) { $requestor = $objects[$phid]; - $handle->setName($requestor->getBestName()); + // TODO: This is currently useless and should be far more informative. + $handle->setName(pht('Requestor %d', $requestor->getID())); + $handle->setURI($requestor->getURI()); } } } diff --git a/src/applications/nuance/query/NuanceItemQuery.php b/src/applications/nuance/query/NuanceItemQuery.php index cc3f79c915..fbcac6e5b7 100644 --- a/src/applications/nuance/query/NuanceItemQuery.php +++ b/src/applications/nuance/query/NuanceItemQuery.php @@ -1,68 +1,85 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } - public function withSourcePHIDs($source_phids) { + public function withSourcePHIDs(array $source_phids) { $this->sourcePHIDs = $source_phids; return $this; } + public function newResultObject() { + return new NuanceItem(); + } + protected function loadPage() { - $table = new NuanceItem(); - $conn = $table->establishConnection('r'); - - $data = queryfx_all( - $conn, - '%Q FROM %T %Q %Q %Q', - $this->buildSelectClause($conn), - $table->getTableName(), - $this->buildWhereClause($conn), - $this->buildOrderClause($conn), - $this->buildLimitClause($conn)); - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); + } + + protected function willFilterPage(array $items) { + $source_phids = mpull($items, 'getSourcePHID'); + + // NOTE: We always load sources, even if the viewer can't formally see + // them. If they can see the item, they're allowed to be aware of the + // source in some sense. + $sources = id(new NuanceSourceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($source_phids) + ->execute(); + $sources = mpull($sources, null, 'getPHID'); + + foreach ($items as $key => $item) { + $source = idx($sources, $item->getSourcePHID()); + if (!$source) { + $this->didRejectResult($items[$key]); + unset($items[$key]); + continue; + } + $item->attachSource($source); + } + + return $items; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->sourcePHIDs !== null) { $where[] = qsprintf( $conn, 'sourcePHID IN (%Ls)', $this->sourcePHIDs); } if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'phid IN (%Ls)', $this->phids); } return $where; } } diff --git a/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php b/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php index 4cc3d7c610..607b0eda6c 100644 --- a/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php +++ b/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php @@ -1,103 +1,132 @@ setName(pht('View Form')) ->setIcon('fa-align-justify') ->setHref($this->getActionURI()); return $actions; } public function updateItems() { return null; } protected function augmentEditForm( AphrontFormView $form, PhabricatorApplicationTransactionValidationException $ex = null) { /* TODO - add a box to allow for custom fields to be defined here, so that * these NuanceSource objects made from this definition can be used to * capture arbitrary data */ return $form; } protected function buildTransactions(AphrontRequest $request) { $transactions = parent::buildTransactions($request); // TODO -- as above return $transactions; } public function renderView() {} public function renderListView() {} public function handleActionRequest(AphrontRequest $request) { $viewer = $request->getViewer(); // TODO: As above, this would eventually be driven by custom logic. if ($request->isFormPost()) { $properties = array( - 'complaint' => (string)$request->getStr('text'), + 'complaint' => (string)$request->getStr('complaint'), ); $content_source = PhabricatorContentSource::newFromRequest($request); $requestor = NuanceRequestor::newFromPhabricatorUser( $viewer, $content_source); $item = $this->newItemFromProperties( $requestor, $properties, $content_source); $uri = $item->getURI(); return id(new AphrontRedirectResponse())->setURI($uri); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendRemarkupInstructions( pht('IMPORTANT: This is a very rough prototype.')) ->appendRemarkupInstructions( pht('Got a complaint? Complain here! We love complaints.')) ->appendChild( id(new AphrontFormTextAreaControl()) ->setName('complaint') ->setLabel(pht('Complaint'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Submit Complaint'))); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Complaint Form')) ->appendChild($form); return $box; } + public function renderItemViewProperties( + PhabricatorUser $viewer, + NuanceItem $item, + PHUIPropertyListView $view) { + $this->renderItemCommonProperties($viewer, $item, $view); + } + + public function renderItemEditProperties( + PhabricatorUser $viewer, + NuanceItem $item, + PHUIPropertyListView $view) { + $this->renderItemCommonProperties($viewer, $item, $view); + } + + private function renderItemCommonProperties( + PhabricatorUser $viewer, + NuanceItem $item, + PHUIPropertyListView $view) { + + $complaint = $item->getNuanceProperty('complaint'); + $complaint = PhabricatorMarkupEngine::renderOneObject( + id(new PhabricatorMarkupOneOff())->setContent($complaint), + 'default', + $viewer); + + $view->addSectionHeader(pht('Complaint')); + $view->addTextContent($complaint); + } + } diff --git a/src/applications/nuance/source/NuanceSourceDefinition.php b/src/applications/nuance/source/NuanceSourceDefinition.php index 2c030badfa..06d302f587 100644 --- a/src/applications/nuance/source/NuanceSourceDefinition.php +++ b/src/applications/nuance/source/NuanceSourceDefinition.php @@ -1,284 +1,298 @@ actor = $actor; return $this; } public function getActor() { return $this->actor; } public function requireActor() { $actor = $this->getActor(); if (!$actor) { throw new PhutilInvalidStateException('setActor'); } return $actor; } public function setSourceObject(NuanceSource $source) { $source->setType($this->getSourceTypeConstant()); $this->sourceObject = $source; return $this; } public function getSourceObject() { return $this->sourceObject; } public function requireSourceObject() { $source = $this->getSourceObject(); if (!$source) { throw new PhutilInvalidStateException('setSourceObject'); } return $source; } public function getSourceViewActions(AphrontRequest $request) { return array(); } public static function getAllDefinitions() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getSourceTypeConstant') ->execute(); } /** * A human readable string like "Twitter" or "Phabricator Form". */ abstract public function getName(); /** * Human readable description of this source, a sentence or two long. */ abstract public function getSourceDescription(); /** * This should be a any VARCHAR(32). * * @{method:getAllDefinitions} will throw if you choose a string that * collides with another @{class:NuanceSourceDefinition} class. */ abstract public function getSourceTypeConstant(); /** * Code to create and update @{class:NuanceItem}s and * @{class:NuanceRequestor}s via daemons goes here. * * If that does not make sense for the @{class:NuanceSource} you are * defining, simply return null. For example, * @{class:NuancePhabricatorFormSourceDefinition} since these are one-way * contact forms. */ abstract public function updateItems(); private function loadSourceObjectPolicies( PhabricatorUser $user, NuanceSource $source) { $user = $this->requireActor(); $source = $this->requireSourceObject(); return id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($source) ->execute(); } final public function getEditTitle() { $source = $this->requireSourceObject(); if ($source->getPHID()) { $title = pht('Edit "%s" source.', $source->getName()); } else { $title = pht('Create a new "%s" source.', $this->getName()); } return $title; } final public function buildEditLayout(AphrontRequest $request) { $actor = $this->requireActor(); $source = $this->requireSourceObject(); $form_errors = array(); $error_messages = array(); $transactions = array(); $validation_exception = null; if ($request->isFormPost()) { $transactions = $this->buildTransactions($request); try { $editor = id(new NuanceSourceEditor()) ->setActor($actor) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->applyTransactions($source, $transactions); return id(new AphrontRedirectResponse()) ->setURI($source->getURI()); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; } } $form = $this->renderEditForm($validation_exception); $layout = id(new PHUIObjectBoxView()) ->setHeaderText($this->getEditTitle()) ->setValidationException($validation_exception) ->setFormErrors($error_messages) ->setForm($form); return $layout; } /** * Code to create a form to edit the @{class:NuanceItem} you are defining. * * return @{class:AphrontFormView} */ private function renderEditForm( PhabricatorApplicationTransactionValidationException $ex = null) { $user = $this->requireActor(); $source = $this->requireSourceObject(); $policies = $this->loadSourceObjectPolicies($user, $source); $e_name = null; if ($ex) { $e_name = $ex->getShortMessage(NuanceSourceTransaction::TYPE_NAME); } $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setError($e_name) ->setValue($source->getName())); $form = $this->augmentEditForm($form, $ex); $form ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($source) ->setPolicies($policies) ->setName('viewPolicy')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($source) ->setPolicies($policies) ->setName('editPolicy')) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($source->getURI()) ->setValue(pht('Save'))); return $form; } /** * return @{class:AphrontFormView} */ protected function augmentEditForm( AphrontFormView $form, PhabricatorApplicationTransactionValidationException $ex = null) { return $form; } /** * Hook to build up @{class:PhabricatorTransactions}. * * return array $transactions */ protected function buildTransactions(AphrontRequest $request) { $transactions = array(); $transactions[] = id(new NuanceSourceTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($request->getStr('editPolicy')); $transactions[] = id(new NuanceSourceTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($request->getStr('viewPolicy')); $transactions[] = id(new NuanceSourceTransaction()) ->setTransactionType(NuanceSourceTransaction::TYPE_NAME) ->setNewvalue($request->getStr('name')); return $transactions; } abstract public function renderView(); abstract public function renderListView(); protected function newItemFromProperties( NuanceRequestor $requestor, array $properties, PhabricatorContentSource $content_source) { // TODO: Should we have a tighter actor/viewer model? Requestors will // often have no real user associated with them... $actor = PhabricatorUser::getOmnipotentUser(); $source = $this->requireSourceObject(); $item = NuanceItem::initializeNewItem(); $xactions = array(); $xactions[] = id(new NuanceItemTransaction()) ->setTransactionType(NuanceItemTransaction::TYPE_SOURCE) ->setNewValue($source->getPHID()); $xactions[] = id(new NuanceItemTransaction()) ->setTransactionType(NuanceItemTransaction::TYPE_REQUESTOR) ->setNewValue($requestor->getPHID()); foreach ($properties as $key => $property) { $xactions[] = id(new NuanceItemTransaction()) ->setTransactionType(NuanceItemTransaction::TYPE_PROPERTY) ->setMetadataValue(NuanceItemTransaction::PROPERTY_KEY, $key) ->setNewValue($property); } $editor = id(new NuanceItemEditor()) ->setActor($actor) ->setActingAsPHID($requestor->getActingAsPHID()) ->setContentSource($content_source); $editor->applyTransactions($item, $xactions); return $item; } + public function renderItemViewProperties( + PhabricatorUser $viewer, + NuanceItem $item, + PHUIPropertyListView $view) { + return; + } + + public function renderItemEditProperties( + PhabricatorUser $viewer, + NuanceItem $item, + PHUIPropertyListView $view) { + return; + } + /* -( Handling Action Requests )------------------------------------------- */ public function handleActionRequest(AphrontRequest $request) { return new Aphront404Response(); } public function getActionURI($path = null) { $source_id = $this->getSourceObject()->getID(); return '/action/'.$source_id.'/'.ltrim($path, '/'); } } diff --git a/src/applications/nuance/storage/NuanceItem.php b/src/applications/nuance/storage/NuanceItem.php index 196afc44ca..2335cd4d42 100644 --- a/src/applications/nuance/storage/NuanceItem.php +++ b/src/applications/nuance/storage/NuanceItem.php @@ -1,171 +1,173 @@ setDateNuanced(time()) ->setStatus(self::STATUS_OPEN); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'data' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'ownerPHID' => 'phid?', 'sourceLabel' => 'text255?', 'status' => 'uint32', 'mailKey' => 'bytes20', 'dateNuanced' => 'epoch', ), self::CONFIG_KEY_SCHEMA => array( 'key_source' => array( 'columns' => array('sourcePHID', 'status', 'dateNuanced', 'id'), ), 'key_owner' => array( 'columns' => array('ownerPHID', 'status', 'dateNuanced', 'id'), ), 'key_contacter' => array( 'columns' => array('requestorPHID', 'status', 'dateNuanced', 'id'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( NuanceItemPHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getURI() { return '/nuance/item/view/'.$this->getID().'/'; } public function getLabel(PhabricatorUser $viewer) { // this is generated at the time the item is created based on // the configuration from the item source. It is typically // something like 'Twitter'. $source_label = $this->getSourceLabel(); return pht( 'Item via %s @ %s.', $source_label, phabricator_datetime($this->getDateCreated(), $viewer)); } public function getRequestor() { return $this->assertAttached($this->requestor); } public function attachRequestor(NuanceRequestor $requestor) { return $this->requestor = $requestor; } public function getSource() { return $this->assertAttached($this->source); } public function attachSource(NuanceSource $source) { $this->source = $source; } public function getNuanceProperty($key, $default = null) { return idx($this->data, $key, $default); } public function setNuanceProperty($key, $value) { $this->data[$key] = $value; return $this; } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { // TODO - this should be based on the queues the item currently resides in return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { // TODO - requestors should get auto access too! return $viewer->getPHID() == $this->ownerPHID; } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht('Owners of an item can always view it.'); case PhabricatorPolicyCapability::CAN_EDIT: return pht('Owners of an item can always edit it.'); } return null; } public function toDictionary() { return array( 'id' => $this->getID(), 'phid' => $this->getPHID(), 'ownerPHID' => $this->getOwnerPHID(), 'requestorPHID' => $this->getRequestorPHID(), 'sourcePHID' => $this->getSourcePHID(), 'sourceLabel' => $this->getSourceLabel(), 'dateCreated' => $this->getDateCreated(), 'dateModified' => $this->getDateModified(), 'dateNuanced' => $this->getDateNuanced(), ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new NuanceItemEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new NuanceItemTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } }