From fdd6351a64a0659019c8438f1a93bbe50bf07c50 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Wed, 22 Jul 2015 13:35:34 -0700 Subject: [PATCH] Moderize Countdown Summary: [DRAFT] Ref T8895. Makes a reasonable attempt at: - Project Support - Timeline / History - Better Search - Better ObjectItemLists Test Plan: Needs better testing (I'm sleepy) Reviewers: btrahan, epriestley Reviewed By: epriestley Subscribers: eadler, epriestley, Korvin Maniphest Tasks: T8895 Differential Revision: https://secure.phabricator.com/D13660 --- .../sql/autopatches/20150719.countdown.1.sql | 36 ++++ .../sql/autopatches/20150719.countdown.2.sql | 2 + .../sql/autopatches/20150719.countdown.3.sql | 2 + src/__phutil_library_map__.php | 15 ++ .../PhabricatorCountdownApplication.php | 6 + ...bricatorCountdownDefaultEditCapability.php | 12 ++ .../PhabricatorCountdownEditController.php | 124 +++++++---- .../PhabricatorCountdownViewController.php | 42 ++-- .../editor/PhabricatorCountdownEditor.php | 193 ++++++++++++++++++ .../mail/PhabricatorCountdownReplyHandler.php | 16 ++ .../query/PhabricatorCountdownQuery.php | 45 ++-- .../PhabricatorCountdownSearchEngine.php | 83 ++++---- .../PhabricatorCountdownTransactionQuery.php | 10 + .../storage/PhabricatorCountdown.php | 54 ++++- .../PhabricatorCountdownSchemaSpec.php | 10 + .../PhabricatorCountdownTransaction.php | 110 ++++++++++ 16 files changed, 630 insertions(+), 130 deletions(-) create mode 100644 resources/sql/autopatches/20150719.countdown.1.sql create mode 100644 resources/sql/autopatches/20150719.countdown.2.sql create mode 100644 resources/sql/autopatches/20150719.countdown.3.sql create mode 100644 src/applications/countdown/capability/PhabricatorCountdownDefaultEditCapability.php create mode 100644 src/applications/countdown/editor/PhabricatorCountdownEditor.php create mode 100644 src/applications/countdown/mail/PhabricatorCountdownReplyHandler.php create mode 100644 src/applications/countdown/query/PhabricatorCountdownTransactionQuery.php create mode 100644 src/applications/countdown/storage/PhabricatorCountdownSchemaSpec.php create mode 100644 src/applications/countdown/storage/PhabricatorCountdownTransaction.php diff --git a/resources/sql/autopatches/20150719.countdown.1.sql b/resources/sql/autopatches/20150719.countdown.1.sql new file mode 100644 index 0000000000..72c039f4c2 --- /dev/null +++ b/resources/sql/autopatches/20150719.countdown.1.sql @@ -0,0 +1,36 @@ +CREATE TABLE {$NAMESPACE}_countdown.countdown_transaction ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64), + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + oldValue LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + newValue LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + contentSource LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + metadata LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + KEY `key_object` (objectPHID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; + +CREATE TABLE {$NAMESPACE}_countdown.edge ( + src VARBINARY(64) NOT NULL, + type INT UNSIGNED NOT NULL, + dst VARBINARY(64) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + seq INT UNSIGNED NOT NULL, + dataID INT UNSIGNED, + PRIMARY KEY (src, type, dst), + KEY `src` (src, type, dateCreated, seq), + UNIQUE KEY `key_dst` (dst, type, src) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; + +CREATE TABLE {$NAMESPACE}_countdown.edgedata ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + data LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT} +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20150719.countdown.2.sql b/resources/sql/autopatches/20150719.countdown.2.sql new file mode 100644 index 0000000000..fbf7dfe235 --- /dev/null +++ b/resources/sql/autopatches/20150719.countdown.2.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_countdown.countdown +ADD editPolicy VARBINARY(64) NOT NULL; diff --git a/resources/sql/autopatches/20150719.countdown.3.sql b/resources/sql/autopatches/20150719.countdown.3.sql new file mode 100644 index 0000000000..29c54fe666 --- /dev/null +++ b/resources/sql/autopatches/20150719.countdown.3.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_countdown.countdown + SET editPolicy = authorPHID WHERE editPolicy = ''; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 514a949753..01356c1c32 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1799,13 +1799,19 @@ phutil_register_library_map(array( 'PhabricatorCountdownController' => 'applications/countdown/controller/PhabricatorCountdownController.php', 'PhabricatorCountdownCountdownPHIDType' => 'applications/countdown/phid/PhabricatorCountdownCountdownPHIDType.php', 'PhabricatorCountdownDAO' => 'applications/countdown/storage/PhabricatorCountdownDAO.php', + 'PhabricatorCountdownDefaultEditCapability' => 'applications/countdown/capability/PhabricatorCountdownDefaultEditCapability.php', 'PhabricatorCountdownDefaultViewCapability' => 'applications/countdown/capability/PhabricatorCountdownDefaultViewCapability.php', 'PhabricatorCountdownDeleteController' => 'applications/countdown/controller/PhabricatorCountdownDeleteController.php', 'PhabricatorCountdownEditController' => 'applications/countdown/controller/PhabricatorCountdownEditController.php', + 'PhabricatorCountdownEditor' => 'applications/countdown/editor/PhabricatorCountdownEditor.php', 'PhabricatorCountdownListController' => 'applications/countdown/controller/PhabricatorCountdownListController.php', 'PhabricatorCountdownQuery' => 'applications/countdown/query/PhabricatorCountdownQuery.php', 'PhabricatorCountdownRemarkupRule' => 'applications/countdown/remarkup/PhabricatorCountdownRemarkupRule.php', + 'PhabricatorCountdownReplyHandler' => 'applications/countdown/mail/PhabricatorCountdownReplyHandler.php', + 'PhabricatorCountdownSchemaSpec' => 'applications/countdown/storage/PhabricatorCountdownSchemaSpec.php', 'PhabricatorCountdownSearchEngine' => 'applications/countdown/query/PhabricatorCountdownSearchEngine.php', + 'PhabricatorCountdownTransaction' => 'applications/countdown/storage/PhabricatorCountdownTransaction.php', + 'PhabricatorCountdownTransactionQuery' => 'applications/countdown/query/PhabricatorCountdownTransactionQuery.php', 'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php', 'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php', 'PhabricatorCredentialsUsedByObjectEdgeType' => 'applications/passphrase/edge/PhabricatorCredentialsUsedByObjectEdgeType.php', @@ -5548,19 +5554,28 @@ phutil_register_library_map(array( 'PhabricatorCountdownDAO', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', + 'PhabricatorSubscribableInterface', + 'PhabricatorApplicationTransactionInterface', 'PhabricatorSpacesInterface', + 'PhabricatorProjectInterface', ), 'PhabricatorCountdownApplication' => 'PhabricatorApplication', 'PhabricatorCountdownController' => 'PhabricatorController', 'PhabricatorCountdownCountdownPHIDType' => 'PhabricatorPHIDType', 'PhabricatorCountdownDAO' => 'PhabricatorLiskDAO', + 'PhabricatorCountdownDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PhabricatorCountdownDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PhabricatorCountdownDeleteController' => 'PhabricatorCountdownController', 'PhabricatorCountdownEditController' => 'PhabricatorCountdownController', + 'PhabricatorCountdownEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorCountdownListController' => 'PhabricatorCountdownController', 'PhabricatorCountdownQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCountdownRemarkupRule' => 'PhabricatorObjectRemarkupRule', + 'PhabricatorCountdownReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', + 'PhabricatorCountdownSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorCountdownSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorCountdownTransaction' => 'PhabricatorApplicationTransaction', + 'PhabricatorCountdownTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorCountdownView' => 'AphrontTagView', 'PhabricatorCountdownViewController' => 'PhabricatorCountdownController', 'PhabricatorCredentialsUsedByObjectEdgeType' => 'PhabricatorEdgeType', diff --git a/src/applications/countdown/application/PhabricatorCountdownApplication.php b/src/applications/countdown/application/PhabricatorCountdownApplication.php index d4b99f9ed7..3ccdb8f327 100644 --- a/src/applications/countdown/application/PhabricatorCountdownApplication.php +++ b/src/applications/countdown/application/PhabricatorCountdownApplication.php @@ -38,6 +38,7 @@ final class PhabricatorCountdownApplication extends PhabricatorApplication { public function getRoutes() { return array( + '/C(?P[1-9]\d*)' => 'PhabricatorCountdownViewController', '/countdown/' => array( '(?:query/(?P[^/]+)/)?' => 'PhabricatorCountdownListController', @@ -55,6 +56,11 @@ final class PhabricatorCountdownApplication extends PhabricatorApplication { 'template' => PhabricatorCountdownCountdownPHIDType::TYPECONST, 'capability' => PhabricatorPolicyCapability::CAN_VIEW, ), + PhabricatorCountdownDefaultEditCapability::CAPABILITY => array( + 'caption' => pht('Default edit policy for new countdowns.'), + 'template' => PhabricatorCountdownCountdownPHIDType::TYPECONST, + 'capability' => PhabricatorPolicyCapability::CAN_EDIT, + ), ); } diff --git a/src/applications/countdown/capability/PhabricatorCountdownDefaultEditCapability.php b/src/applications/countdown/capability/PhabricatorCountdownDefaultEditCapability.php new file mode 100644 index 0000000000..b1d8fd7b31 --- /dev/null +++ b/src/applications/countdown/capability/PhabricatorCountdownDefaultEditCapability.php @@ -0,0 +1,12 @@ +id = idx($data, 'id'); - } + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); - public function processRequest() { - - $request = $this->getRequest(); - $user = $request->getUser(); - - if ($this->id) { + if ($id) { $page_title = pht('Edit Countdown'); $countdown = id(new PhabricatorCountdownQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, @@ -28,12 +22,18 @@ final class PhabricatorCountdownEditController return new Aphront404Response(); } $date_value = AphrontFormDateControlValue::newFromEpoch( - $user, + $viewer, $countdown->getEpoch()); + $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( + $countdown->getPHID(), + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); + $v_projects = array_reverse($v_projects); } else { $page_title = pht('Create Countdown'); - $countdown = PhabricatorCountdown::initializeNewCountdown($user); - $date_value = AphrontFormDateControlValue::newFromEpoch($user, time()); + $countdown = PhabricatorCountdown::initializeNewCountdown($viewer); + $date_value = AphrontFormDateControlValue::newFromEpoch( + $viewer, PhabricatorTime::getNow()); + $v_projects = array(); } $errors = array(); @@ -42,6 +42,8 @@ final class PhabricatorCountdownEditController $v_text = $countdown->getTitle(); $v_space = $countdown->getSpacePHID(); + $v_view = $countdown->getViewPolicy(); + $v_edit = $countdown->getEditPolicy(); if ($request->isFormPost()) { $v_text = $request->getStr('title'); @@ -49,27 +51,61 @@ final class PhabricatorCountdownEditController $date_value = AphrontFormDateControlValue::newFromRequest( $request, 'epoch'); - $view_policy = $request->getStr('viewPolicy'); + $v_view = $request->getStr('viewPolicy'); + $v_edit = $request->getStr('editPolicy'); + $v_projects = $request->getArr('projects'); - $e_text = null; - if (!strlen($v_text)) { - $e_text = pht('Required'); - $errors[] = pht('You must give the countdown a name.'); - } - if (!$date_value->isValid()) { - $e_epoch = pht('Invalid'); - $errors[] = pht('You must give the countdown a valid end date.'); - } + $type_title = PhabricatorCountdownTransaction::TYPE_TITLE; + $type_epoch = PhabricatorCountdownTransaction::TYPE_EPOCH; + $type_space = PhabricatorTransactions::TYPE_SPACE; + $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; + $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; + + $xactions = array(); + + $xactions[] = id(new PhabricatorCountdownTransaction()) + ->setTransactionType($type_title) + ->setNewValue($v_text); + + $xactions[] = id(new PhabricatorCountdownTransaction()) + ->setTransactionType($type_epoch) + ->setNewValue($date_value); + + $xactions[] = id(new PhabricatorCountdownTransaction()) + ->setTransactionType($type_space) + ->setNewValue($v_space); + + $xactions[] = id(new PhabricatorCountdownTransaction()) + ->setTransactionType($type_view) + ->setNewValue($v_view); + + $xactions[] = id(new PhabricatorCountdownTransaction()) + ->setTransactionType($type_edit) + ->setNewValue($v_edit); + + $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; + $xactions[] = id(new PhabricatorCountdownTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $proj_edge_type) + ->setNewValue(array('=' => array_fuse($v_projects))); + + $editor = id(new PhabricatorCountdownEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + try { + $editor->applyTransactions($countdown, $xactions); - if (!count($errors)) { - $countdown->setTitle($v_text); - $countdown->setEpoch($date_value->getEpoch()); - $countdown->setViewPolicy($view_policy); - $countdown->setSpacePHID($v_space); - $countdown->save(); return id(new AphrontRedirectResponse()) - ->setURI('/countdown/'.$countdown->getID().'/'); + ->setURI('/'.$countdown->getMonogram()); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; + + $e_title = $ex->getShortMessage($type_title); + $e_epoch = $ex->getShortMessage($type_epoch); } + } $crumbs = $this->buildApplicationCrumbs(); @@ -86,12 +122,12 @@ final class PhabricatorCountdownEditController } $policies = id(new PhabricatorPolicyQuery()) - ->setViewer($user) + ->setViewer($viewer) ->setObject($countdown) ->execute(); $form = id(new AphrontFormView()) - ->setUser($user) + ->setUser($viewer) ->setAction($request->getRequestURI()->getPath()) ->appendChild( id(new AphrontFormTextControl()) @@ -99,21 +135,33 @@ final class PhabricatorCountdownEditController ->setValue($v_text) ->setName('title') ->setError($e_text)) - ->appendChild( + ->appendControl( id(new AphrontFormDateControl()) - ->setUser($user) ->setName('epoch') ->setLabel(pht('End Date')) ->setError($e_epoch) ->setValue($date_value)) - ->appendChild( + ->appendControl( id(new AphrontFormPolicyControl()) - ->setUser($user) ->setName('viewPolicy') ->setPolicyObject($countdown) ->setPolicies($policies) ->setSpacePHID($v_space) + ->setValue($v_view) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) + ->appendControl( + id(new AphrontFormPolicyControl()) + ->setName('editPolicy') + ->setPolicyObject($countdown) + ->setPolicies($policies) + ->setValue($v_edit) + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setLabel(pht('Projects')) + ->setName('projects') + ->setValue($v_projects) + ->setDatasource(new PhabricatorProjectDatasource())) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) diff --git a/src/applications/countdown/controller/PhabricatorCountdownViewController.php b/src/applications/countdown/controller/PhabricatorCountdownViewController.php index 62414b9860..526a485138 100644 --- a/src/applications/countdown/controller/PhabricatorCountdownViewController.php +++ b/src/applications/countdown/controller/PhabricatorCountdownViewController.php @@ -3,31 +3,24 @@ final class PhabricatorCountdownViewController extends PhabricatorCountdownController { - private $id; - public function shouldAllowPublic() { return true; } - public function willProcessRequest(array $data) { - $this->id = $data['id']; - } - - public function processRequest() { - - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $countdown = id(new PhabricatorCountdownQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); if (!$countdown) { return new Aphront404Response(); } $countdown_view = id(new PhabricatorCountdownView()) - ->setUser($user) + ->setUser($viewer) ->setCountdown($countdown) ->setHeadless(true); @@ -38,10 +31,22 @@ final class PhabricatorCountdownViewController ->buildApplicationCrumbs() ->addTextCrumb("C{$id}"); + $epoch = $countdown->getEpoch(); + if ($epoch >= PhabricatorTime::getNow()) { + $icon = 'fa-clock-o'; + $color = ''; + $status = pht('Running'); + } else { + $icon = 'fa-check-square-o'; + $color = 'dark'; + $status = pht('Launched'); + } + $header = id(new PHUIHeaderView()) ->setHeader($title) - ->setUser($user) - ->setPolicyObject($countdown); + ->setUser($viewer) + ->setPolicyObject($countdown) + ->setStatus($icon, $color, $status); $actions = $this->buildActionListView($countdown); $properties = $this->buildPropertyListView($countdown, $actions); @@ -50,10 +55,16 @@ final class PhabricatorCountdownViewController ->setHeader($header) ->addPropertyList($properties); + $timeline = $this->buildTransactionTimeline( + $countdown, + new PhabricatorCountdownTransactionQuery()); + $timeline->setShouldTerminate(true); + $content = array( $crumbs, $object_box, $countdown_view, + $timeline, ); return $this->buildApplicationPage( @@ -105,6 +116,7 @@ final class PhabricatorCountdownViewController $view = id(new PHUIPropertyListView()) ->setUser($viewer) + ->setObject($countdown) ->setActionList($actions); $view->addProperty( diff --git a/src/applications/countdown/editor/PhabricatorCountdownEditor.php b/src/applications/countdown/editor/PhabricatorCountdownEditor.php new file mode 100644 index 0000000000..99e38b8eeb --- /dev/null +++ b/src/applications/countdown/editor/PhabricatorCountdownEditor.php @@ -0,0 +1,193 @@ +getTransactionType()) { + case PhabricatorCountdownTransaction::TYPE_TITLE: + return $object->getTitle(); + case PhabricatorCountdownTransaction::TYPE_EPOCH: + return $object->getEpoch(); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhabricatorCountdownTransaction::TYPE_TITLE: + return $xaction->getNewValue(); + case PhabricatorCountdownTransaction::TYPE_EPOCH: + return $xaction->getNewValue()->getEpoch(); + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + $type = $xaction->getTransactionType(); + switch ($type) { + case PhabricatorCountdownTransaction::TYPE_TITLE: + $object->setTitle($xaction->getNewValue()); + return; + case PhabricatorCountdownTransaction::TYPE_EPOCH: + $object->setEpoch($xaction->getNewValue()); + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + $type = $xaction->getTransactionType(); + switch ($type) { + case PhabricatorCountdownTransaction::TYPE_TITLE: + return; + case PhabricatorCountdownTransaction::TYPE_EPOCH: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + switch ($type) { + case PhabricatorCountdownTransaction::TYPE_TITLE: + $missing = $this->validateIsEmptyTextField( + $object->getTitle(), + $xactions); + + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('You must give the countdown a name.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } + break; + case PhabricatorCountdownTransaction::TYPE_EPOCH: + $date_value = AphrontFormDateControlValue::newFromEpoch( + $this->requireActor(), + $object->getEpoch()); + if (!$date_value->isValid()) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht('You must give the countdown a valid end date.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } + break; + } + + return $errors; + } + + protected function shouldSendMail( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + + public function getMailTagsMap() { + return array( + PhabricatorCountdownTransaction::MAILTAG_TITLE => + pht('Someone changes the countdown title.'), + PhabricatorCountdownTransaction::MAILTAG_EPOCH => + pht('Someone changes the countdown end date.'), + PhabricatorCountdownTransaction::MAILTAG_OTHER => + pht('Other countdown activity not listed above occurs.'), + ); + } + + protected function buildMailTemplate(PhabricatorLiskDAO $object) { + $monogram = $object->getMonogram(); + $name = $object->getName(); + + return id(new PhabricatorMetaMTAMail()) + ->setSubject("{$monogram}: {$name}") + ->addHeader('Thread-Topic', $monogram); + } + + protected function buildMailBody( + PhabricatorLiskDAO $object, + array $xactions) { + + $body = parent::buildMailBody($object, $xactions); + + $body->addLinkSection( + pht('COUNTDOWN DETAIL'), + PhabricatorEnv::getProductionURI('/'.$object->getMonogram())); + + return $body; + } + + protected function getMailTo(PhabricatorLiskDAO $object) { + return array($object->getAuthorPHID()); + } + + protected function getMailSubjectPrefix() { + return 'Countdown'; + } + + protected function buildReplyHandler(PhabricatorLiskDAO $object) { + return id(new PhabricatorCountdownReplyHandler()) + ->setMailReceiver($object); + } + + protected function shouldPublishFeedStory( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + + protected function supportsSearch() { + return true; + } + +} diff --git a/src/applications/countdown/mail/PhabricatorCountdownReplyHandler.php b/src/applications/countdown/mail/PhabricatorCountdownReplyHandler.php new file mode 100644 index 0000000000..51932d0a14 --- /dev/null +++ b/src/applications/countdown/mail/PhabricatorCountdownReplyHandler.php @@ -0,0 +1,16 @@ +upcoming = true; return $this; } protected function loadPage() { - $table = new PhabricatorCountdown(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - $countdowns = $table->loadAllFromArray($data); - - return $countdowns; + return $this->loadStandardPage($this->newResultObject()); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + public function newResultObject() { + return new PhabricatorCountdown(); + } - $where[] = $this->buildPagingClause($conn_r); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } - if ($this->authorPHIDs) { + if ($this->authorPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'authorPHID in (%Ls)', $this->authorPHIDs); } - if ($this->upcoming) { + if ($this->upcoming !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'epoch >= %d', - time()); + PhabricatorTime::getNow()); } - return $this->formatWhereClause($where); + return $where; } public function getQueryApplicationClass() { diff --git a/src/applications/countdown/query/PhabricatorCountdownSearchEngine.php b/src/applications/countdown/query/PhabricatorCountdownSearchEngine.php index 2b38154dda..74ee2b9518 100644 --- a/src/applications/countdown/query/PhabricatorCountdownSearchEngine.php +++ b/src/applications/countdown/query/PhabricatorCountdownSearchEngine.php @@ -11,53 +11,39 @@ final class PhabricatorCountdownSearchEngine return 'PhabricatorCountdownApplication'; } - public function buildSavedQueryFromRequest(AphrontRequest $request) { - $saved = new PhabricatorSavedQuery(); - $saved->setParameter( - 'authorPHIDs', - $this->readUsersFromRequest($request, 'authors')); - - $saved->setParameter('upcoming', $request->getBool('upcoming')); - - return $saved; + public function newQuery() { + return new PhabricatorCountdownQuery(); } - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new PhabricatorCountdownQuery()); + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); - $author_phids = $saved->getParameter('authorPHIDs', array()); - if ($author_phids) { - $query->withAuthorPHIDs($author_phids); + if ($map['authorPHIDs']) { + $query->withAuthorPHIDs($map['authorPHIDs']); } - if ($saved->getParameter('upcoming')) { - $query->withUpcoming(true); + if ($map['upcoming'] && $map['upcoming'][0] == 'upcoming') { + $query->withUpcoming(); } return $query; } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved_query) { + protected function buildCustomSearchFields() { - $author_phids = $saved_query->getParameter('authorPHIDs', array()); - $upcoming = $saved_query->getParameter('upcoming'); - - $form - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorPeopleDatasource()) - ->setName('authors') + return array( + id(new PhabricatorUsersSearchField()) ->setLabel(pht('Authors')) - ->setValue($author_phids)) - ->appendChild( - id(new AphrontFormCheckboxControl()) - ->addCheckbox( - 'upcoming', - 1, - pht('Show only countdowns that are still counting down.'), - $upcoming)); + ->setKey('authorPHIDs') + ->setAliases(array('author', 'authors')), + + id(new PhabricatorSearchCheckboxesField()) + ->setKey('upcoming') + ->setOptions(array( + 'upcoming' => pht('Show only upcoming countdowns.'), + )), + ); + } protected function getURI($path) { @@ -89,7 +75,7 @@ final class PhabricatorCountdownSearchEngine 'authorPHIDs', array($this->requireViewer()->getPHID())); case 'upcoming': - return $query->setParameter('upcoming', true); + return $query->setParameter('upcoming', array('upcoming')); } return parent::buildSavedQueryFromBuiltin($query_key); @@ -115,28 +101,35 @@ final class PhabricatorCountdownSearchEngine $list->setUser($viewer); foreach ($countdowns as $countdown) { $id = $countdown->getID(); + $ended = false; + $icon = 'fa-clock-o'; + $color = 'green'; + $epoch = $countdown->getEpoch(); + if ($epoch <= PhabricatorTime::getNow()) { + $ended = true; + $icon = 'fa-check-square-o'; + $color = 'grey'; + } $item = id(new PHUIObjectItemView()) ->setUser($viewer) ->setObject($countdown) ->setObjectName("C{$id}") ->setHeader($countdown->getTitle()) + ->setStatusIcon($icon.' '.$color) ->setHref($this->getApplicationURI("{$id}/")) ->addByline( pht( 'Created by %s', $handles[$countdown->getAuthorPHID()]->renderLink())); - $epoch = $countdown->getEpoch(); - if ($epoch >= time()) { - $item->addIcon( - 'none', - pht('Ends %s', phabricator_datetime($epoch, $viewer))); - } else { - $item->addIcon( - 'delete', - pht('Ended %s', phabricator_datetime($epoch, $viewer))); + if ($ended) { + $item->addAttribute( + pht('Launched on %s', phabricator_datetime($epoch, $viewer))); $item->setDisabled(true); + } else { + $item->addAttribute( + phabricator_datetime($epoch, $viewer)); } $list->addItem($item); diff --git a/src/applications/countdown/query/PhabricatorCountdownTransactionQuery.php b/src/applications/countdown/query/PhabricatorCountdownTransactionQuery.php new file mode 100644 index 0000000000..11c10a72eb --- /dev/null +++ b/src/applications/countdown/query/PhabricatorCountdownTransactionQuery.php @@ -0,0 +1,10 @@ +getID(); + } + + +/* -( PhabricatorSubscribableInterface )----------------------------------- */ + + + public function isAutomaticallySubscribed($phid) { + return ($phid == $this->getAuthorPHID()); + } + + public function shouldShowSubscribersProperty() { + return true; + } + + public function shouldAllowSubscription($phid) { + return true; + } + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorCountdownEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorCountdownTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + + return $timeline; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -59,16 +105,16 @@ final class PhabricatorCountdown extends PhabricatorCountdownDAO case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: - return PhabricatorPolicies::POLICY_NOONE; + return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - return ($viewer->getPHID() == $this->getAuthorPHID()); + return false; } public function describeAutomaticCapability($capability) { - return pht('The author of a countdown can always view and edit it.'); + return false; } /* -( PhabricatorSpacesInterface )------------------------------------------- */ diff --git a/src/applications/countdown/storage/PhabricatorCountdownSchemaSpec.php b/src/applications/countdown/storage/PhabricatorCountdownSchemaSpec.php new file mode 100644 index 0000000000..63d057b86f --- /dev/null +++ b/src/applications/countdown/storage/PhabricatorCountdownSchemaSpec.php @@ -0,0 +1,10 @@ +buildEdgeSchemata(new PhabricatorCountdown()); + } + +} diff --git a/src/applications/countdown/storage/PhabricatorCountdownTransaction.php b/src/applications/countdown/storage/PhabricatorCountdownTransaction.php new file mode 100644 index 0000000000..7433b24427 --- /dev/null +++ b/src/applications/countdown/storage/PhabricatorCountdownTransaction.php @@ -0,0 +1,110 @@ +getAuthorPHID(); + $object_phid = $this->getObjectPHID(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $type = $this->getTransactionType(); + switch ($type) { + case self::TYPE_TITLE: + if ($old === null) { + return pht( + '%s created this countdown.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s renamed this countdown from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old, + $new); + } + case self::TYPE_EPOCH: + if ($old === null) { + return pht( + '%s set this countdown to end on %s.', + $this->renderHandleLink($author_phid), + phabricator_datetime($new, $this->getViewer())); + } else if ($old != $new) { + return pht( + '%s updated this countdown to end on %s.', + $this->renderHandleLink($author_phid), + phabricator_datetime($new, $this->getViewer())); + } + break; + } + + return parent::getTitle(); + } + + public function getTitleForFeed() { + $author_phid = $this->getAuthorPHID(); + $object_phid = $this->getObjectPHID(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $type = $this->getTransactionType(); + switch ($type) { + case self::TYPE_TITLE: + if ($old === null) { + return pht( + '%s created %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + + } else { + return pht( + '%s renamed %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } + break; + } + + return parent::getTitleForFeed(); + } + + public function getMailTags() { + $tags = parent::getMailTags(); + + switch ($this->getTransactionType()) { + case self::TYPE_TITLE: + $tags[] = self::MAILTAG_TITLE; + break; + case self::TYPE_EPOCH: + $tags[] = self::MAILTAG_EPOCH; + break; + default: + $tags[] = self::MAILTAG_OTHER; + break; + } + + return $tags; + } + +}