@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
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Convert Countdown to EditEngine

Summary: Fixes T10684. Fixes T10520. This primarily implements a date/epoch field, and then does a bunch of standard plumbing.

Test Plan:
- Created countdowns.
- Edited countdowns.
- Used HTTP prefilling.
- Created a countdown ending on "Christmas Morning", etc.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10520, T10684

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

+366 -456
+6 -2
src/__phutil_library_map__.php
··· 139 139 'AphrontDefaultApplicationConfiguration' => 'aphront/configuration/AphrontDefaultApplicationConfiguration.php', 140 140 'AphrontDialogResponse' => 'aphront/response/AphrontDialogResponse.php', 141 141 'AphrontDialogView' => 'view/AphrontDialogView.php', 142 + 'AphrontEpochHTTPParameterType' => 'aphront/httpparametertype/AphrontEpochHTTPParameterType.php', 142 143 'AphrontException' => 'aphront/exception/AphrontException.php', 143 144 'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php', 144 145 'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php', ··· 2100 2101 'PhabricatorCoreConfigOptions' => 'applications/config/option/PhabricatorCoreConfigOptions.php', 2101 2102 'PhabricatorCountdown' => 'applications/countdown/storage/PhabricatorCountdown.php', 2102 2103 'PhabricatorCountdownApplication' => 'applications/countdown/application/PhabricatorCountdownApplication.php', 2103 - 'PhabricatorCountdownCommentController' => 'applications/countdown/controller/PhabricatorCountdownCommentController.php', 2104 2104 'PhabricatorCountdownController' => 'applications/countdown/controller/PhabricatorCountdownController.php', 2105 2105 'PhabricatorCountdownCountdownPHIDType' => 'applications/countdown/phid/PhabricatorCountdownCountdownPHIDType.php', 2106 2106 'PhabricatorCountdownDAO' => 'applications/countdown/storage/PhabricatorCountdownDAO.php', ··· 2108 2108 'PhabricatorCountdownDefaultViewCapability' => 'applications/countdown/capability/PhabricatorCountdownDefaultViewCapability.php', 2109 2109 'PhabricatorCountdownDeleteController' => 'applications/countdown/controller/PhabricatorCountdownDeleteController.php', 2110 2110 'PhabricatorCountdownEditController' => 'applications/countdown/controller/PhabricatorCountdownEditController.php', 2111 + 'PhabricatorCountdownEditEngine' => 'applications/countdown/editor/PhabricatorCountdownEditEngine.php', 2111 2112 'PhabricatorCountdownEditor' => 'applications/countdown/editor/PhabricatorCountdownEditor.php', 2112 2113 'PhabricatorCountdownListController' => 'applications/countdown/controller/PhabricatorCountdownListController.php', 2113 2114 'PhabricatorCountdownMailReceiver' => 'applications/countdown/mail/PhabricatorCountdownMailReceiver.php', ··· 2325 2326 'PhabricatorEmptyQueryException' => 'infrastructure/query/PhabricatorEmptyQueryException.php', 2326 2327 'PhabricatorEnv' => 'infrastructure/env/PhabricatorEnv.php', 2327 2328 'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__/PhabricatorEnvTestCase.php', 2329 + 'PhabricatorEpochEditField' => 'applications/transactions/editfield/PhabricatorEpochEditField.php', 2328 2330 'PhabricatorEvent' => 'infrastructure/events/PhabricatorEvent.php', 2329 2331 'PhabricatorEventEngine' => 'infrastructure/events/PhabricatorEventEngine.php', 2330 2332 'PhabricatorEventListener' => 'infrastructure/events/PhabricatorEventListener.php', ··· 4261 4263 'AphrontView', 4262 4264 'AphrontResponseProducerInterface', 4263 4265 ), 4266 + 'AphrontEpochHTTPParameterType' => 'AphrontHTTPParameterType', 4264 4267 'AphrontException' => 'Exception', 4265 4268 'AphrontFileResponse' => 'AphrontResponse', 4266 4269 'AphrontFormCheckboxControl' => 'AphrontFormControl', ··· 6525 6528 'PhabricatorProjectInterface', 6526 6529 ), 6527 6530 'PhabricatorCountdownApplication' => 'PhabricatorApplication', 6528 - 'PhabricatorCountdownCommentController' => 'PhabricatorCountdownController', 6529 6531 'PhabricatorCountdownController' => 'PhabricatorController', 6530 6532 'PhabricatorCountdownCountdownPHIDType' => 'PhabricatorPHIDType', 6531 6533 'PhabricatorCountdownDAO' => 'PhabricatorLiskDAO', ··· 6533 6535 'PhabricatorCountdownDefaultViewCapability' => 'PhabricatorPolicyCapability', 6534 6536 'PhabricatorCountdownDeleteController' => 'PhabricatorCountdownController', 6535 6537 'PhabricatorCountdownEditController' => 'PhabricatorCountdownController', 6538 + 'PhabricatorCountdownEditEngine' => 'PhabricatorEditEngine', 6536 6539 'PhabricatorCountdownEditor' => 'PhabricatorApplicationTransactionEditor', 6537 6540 'PhabricatorCountdownListController' => 'PhabricatorCountdownController', 6538 6541 'PhabricatorCountdownMailReceiver' => 'PhabricatorObjectMailReceiver', ··· 6776 6779 'PhabricatorEmptyQueryException' => 'Exception', 6777 6780 'PhabricatorEnv' => 'Phobject', 6778 6781 'PhabricatorEnvTestCase' => 'PhabricatorTestCase', 6782 + 'PhabricatorEpochEditField' => 'PhabricatorEditField', 6779 6783 'PhabricatorEvent' => 'PhutilEvent', 6780 6784 'PhabricatorEventEngine' => 'Phobject', 6781 6785 'PhabricatorEventListener' => 'PhutilEventListener',
+37
src/aphront/httpparametertype/AphrontEpochHTTPParameterType.php
··· 1 + <?php 2 + 3 + final class AphrontEpochHTTPParameterType 4 + extends AphrontHTTPParameterType { 5 + 6 + protected function getParameterExists(AphrontRequest $request, $key) { 7 + return $request->getExists($key) || 8 + $request->getExists($key.'_d'); 9 + } 10 + 11 + protected function getParameterValue(AphrontRequest $request, $key) { 12 + return AphrontFormDateControlValue::newFromRequest($request, $key); 13 + } 14 + 15 + protected function getParameterTypeName() { 16 + return 'epoch'; 17 + } 18 + 19 + protected function getParameterFormatDescriptions() { 20 + return array( 21 + pht('An epoch timestamp, as an integer.'), 22 + pht('An absolute date, as a string.'), 23 + pht('A relative date, as a string.'), 24 + pht('Separate date and time inputs, as strings.'), 25 + ); 26 + } 27 + 28 + protected function getParameterExamples() { 29 + return array( 30 + 'v=1460050737', 31 + 'v=2022-01-01', 32 + 'v=yesterday', 33 + 'v_d=2022-01-01&v_t=12:34', 34 + ); 35 + } 36 + 37 + }
+1 -3
src/applications/countdown/application/PhabricatorCountdownApplication.php
··· 46 46 => 'PhabricatorCountdownViewController', 47 47 'comment/(?P<id>[1-9]\d*)/' 48 48 => 'PhabricatorCountdownCommentController', 49 - 'edit/(?:(?P<id>[1-9]\d*)/)?' 50 - => 'PhabricatorCountdownEditController', 51 - 'create/' 49 + $this->getEditRoutePattern('edit/') 52 50 => 'PhabricatorCountdownEditController', 53 51 'delete/(?P<id>[1-9]\d*)/' 54 52 => 'PhabricatorCountdownDeleteController',
-63
src/applications/countdown/controller/PhabricatorCountdownCommentController.php
··· 1 - <?php 2 - 3 - final class PhabricatorCountdownCommentController 4 - extends PhabricatorCountdownController { 5 - 6 - public function handleRequest(AphrontRequest $request) { 7 - $viewer = $request->getViewer(); 8 - $id = $request->getURIData('id'); 9 - 10 - if (!$request->isFormPost()) { 11 - return new Aphront400Response(); 12 - } 13 - 14 - $countdown = id(new PhabricatorCountdownQuery()) 15 - ->setViewer($viewer) 16 - ->withIDs(array($id)) 17 - ->executeOne(); 18 - if (!$countdown) { 19 - return new Aphront404Response(); 20 - } 21 - 22 - $is_preview = $request->isPreviewRequest(); 23 - $draft = PhabricatorDraft::buildFromRequest($request); 24 - 25 - $view_uri = '/'.$countdown->getMonogram(); 26 - 27 - $xactions = array(); 28 - $xactions[] = id(new PhabricatorCountdownTransaction()) 29 - ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) 30 - ->attachComment( 31 - id(new PhabricatorCountdownTransactionComment()) 32 - ->setContent($request->getStr('comment'))); 33 - 34 - $editor = id(new PhabricatorCountdownEditor()) 35 - ->setActor($viewer) 36 - ->setContinueOnNoEffect($request->isContinueRequest()) 37 - ->setContentSourceFromRequest($request) 38 - ->setIsPreview($is_preview); 39 - 40 - try { 41 - $xactions = $editor->applyTransactions($countdown, $xactions); 42 - } catch (PhabricatorApplicationTransactionNoEffectException $ex) { 43 - return id(new PhabricatorApplicationTransactionNoEffectResponse()) 44 - ->setCancelURI($view_uri) 45 - ->setException($ex); 46 - } 47 - 48 - if ($draft) { 49 - $draft->replaceOrDelete(); 50 - } 51 - 52 - if ($request->isAjax() && $is_preview) { 53 - return id(new PhabricatorApplicationTransactionResponse()) 54 - ->setViewer($viewer) 55 - ->setTransactions($xactions) 56 - ->setIsPreview($is_preview); 57 - } else { 58 - return id(new AphrontRedirectResponse()) 59 - ->setURI($view_uri); 60 - } 61 - } 62 - 63 - }
-11
src/applications/countdown/controller/PhabricatorCountdownController.php
··· 7 7 ->setSearchEngine(new PhabricatorCountdownSearchEngine()); 8 8 } 9 9 10 - protected function buildApplicationCrumbs() { 11 - $crumbs = parent::buildApplicationCrumbs(); 12 - 13 - $crumbs->addAction( 14 - id(new PHUIListItemView()) 15 - ->setName(pht('Create Countdown')) 16 - ->setHref($this->getApplicationURI('create/')) 17 - ->setIcon('fa-plus-square')); 18 - 19 - return $crumbs; 20 - } 21 10 22 11 }
+3 -199
src/applications/countdown/controller/PhabricatorCountdownEditController.php
··· 4 4 extends PhabricatorCountdownController { 5 5 6 6 public function handleRequest(AphrontRequest $request) { 7 - $viewer = $request->getViewer(); 8 - $id = $request->getURIData('id'); 9 - 10 - if ($id) { 11 - $countdown = id(new PhabricatorCountdownQuery()) 12 - ->setViewer($viewer) 13 - ->withIDs(array($id)) 14 - ->requireCapabilities( 15 - array( 16 - PhabricatorPolicyCapability::CAN_VIEW, 17 - PhabricatorPolicyCapability::CAN_EDIT, 18 - )) 19 - ->executeOne(); 20 - if (!$countdown) { 21 - return new Aphront404Response(); 22 - } 23 - $date_value = AphrontFormDateControlValue::newFromEpoch( 24 - $viewer, 25 - $countdown->getEpoch()); 26 - $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( 27 - $countdown->getPHID(), 28 - PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); 29 - $v_projects = array_reverse($v_projects); 30 - $title = pht('Edit Countdown: %s', $countdown->getTitle()); 31 - } else { 32 - $title = pht('Create Countdown'); 33 - $countdown = PhabricatorCountdown::initializeNewCountdown($viewer); 34 - $date_value = AphrontFormDateControlValue::newFromEpoch( 35 - $viewer, PhabricatorTime::getNow()); 36 - $v_projects = array(); 37 - } 38 - 39 - $errors = array(); 40 - $e_text = true; 41 - $e_epoch = null; 42 - 43 - $v_text = $countdown->getTitle(); 44 - $v_desc = $countdown->getDescription(); 45 - $v_space = $countdown->getSpacePHID(); 46 - $v_view = $countdown->getViewPolicy(); 47 - $v_edit = $countdown->getEditPolicy(); 48 - 49 - if ($request->isFormPost()) { 50 - $v_text = $request->getStr('title'); 51 - $v_desc = $request->getStr('description'); 52 - $v_space = $request->getStr('spacePHID'); 53 - $date_value = AphrontFormDateControlValue::newFromRequest( 54 - $request, 55 - 'epoch'); 56 - $v_view = $request->getStr('viewPolicy'); 57 - $v_edit = $request->getStr('editPolicy'); 58 - $v_projects = $request->getArr('projects'); 59 - 60 - $type_title = PhabricatorCountdownTransaction::TYPE_TITLE; 61 - $type_epoch = PhabricatorCountdownTransaction::TYPE_EPOCH; 62 - $type_description = PhabricatorCountdownTransaction::TYPE_DESCRIPTION; 63 - $type_space = PhabricatorTransactions::TYPE_SPACE; 64 - $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; 65 - $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; 66 - 67 - $xactions = array(); 68 - 69 - $xactions[] = id(new PhabricatorCountdownTransaction()) 70 - ->setTransactionType($type_title) 71 - ->setNewValue($v_text); 72 - 73 - $xactions[] = id(new PhabricatorCountdownTransaction()) 74 - ->setTransactionType($type_epoch) 75 - ->setNewValue($date_value); 76 - 77 - $xactions[] = id(new PhabricatorCountdownTransaction()) 78 - ->setTransactionType($type_description) 79 - ->setNewValue($v_desc); 80 - 81 - $xactions[] = id(new PhabricatorCountdownTransaction()) 82 - ->setTransactionType($type_space) 83 - ->setNewValue($v_space); 84 - 85 - $xactions[] = id(new PhabricatorCountdownTransaction()) 86 - ->setTransactionType($type_view) 87 - ->setNewValue($v_view); 88 - 89 - $xactions[] = id(new PhabricatorCountdownTransaction()) 90 - ->setTransactionType($type_edit) 91 - ->setNewValue($v_edit); 92 - 93 - $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; 94 - $xactions[] = id(new PhabricatorCountdownTransaction()) 95 - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 96 - ->setMetadataValue('edge:type', $proj_edge_type) 97 - ->setNewValue(array('=' => array_fuse($v_projects))); 98 - 99 - $editor = id(new PhabricatorCountdownEditor()) 100 - ->setActor($viewer) 101 - ->setContentSourceFromRequest($request) 102 - ->setContinueOnNoEffect(true); 103 - 104 - try { 105 - $editor->applyTransactions($countdown, $xactions); 106 - 107 - return id(new AphrontRedirectResponse()) 108 - ->setURI('/'.$countdown->getMonogram()); 109 - } catch (PhabricatorApplicationTransactionValidationException $ex) { 110 - $validation_exception = $ex; 111 - 112 - $e_title = $ex->getShortMessage($type_title); 113 - $e_epoch = $ex->getShortMessage($type_epoch); 114 - } 115 - 116 - } 117 - 118 - $crumbs = $this->buildApplicationCrumbs(); 119 - $crumbs->setBorder(true); 120 - 121 - $cancel_uri = '/countdown/'; 122 - if ($countdown->getID()) { 123 - $cancel_uri = '/countdown/'.$countdown->getID().'/'; 124 - $crumbs->addTextCrumb('C'.$countdown->getID(), $cancel_uri); 125 - $crumbs->addTextCrumb(pht('Edit')); 126 - $submit_label = pht('Save Changes'); 127 - $header_icon = 'fa-pencil'; 128 - } else { 129 - $crumbs->addTextCrumb(pht('Create Countdown')); 130 - $submit_label = pht('Create Countdown'); 131 - $header_icon = 'fa-plus-square'; 132 - } 133 - 134 - $policies = id(new PhabricatorPolicyQuery()) 135 - ->setViewer($viewer) 136 - ->setObject($countdown) 137 - ->execute(); 138 - 139 - $form = id(new AphrontFormView()) 140 - ->setUser($viewer) 141 - ->setAction($request->getRequestURI()->getPath()) 142 - ->appendChild( 143 - id(new AphrontFormTextControl()) 144 - ->setLabel(pht('Title')) 145 - ->setValue($v_text) 146 - ->setName('title') 147 - ->setError($e_text)) 148 - ->appendControl( 149 - id(new AphrontFormDateControl()) 150 - ->setName('epoch') 151 - ->setLabel(pht('End Date')) 152 - ->setError($e_epoch) 153 - ->setValue($date_value)) 154 - ->appendControl( 155 - id(new PhabricatorRemarkupControl()) 156 - ->setName('description') 157 - ->setLabel(pht('Description')) 158 - ->setValue($v_desc)) 159 - ->appendControl( 160 - id(new AphrontFormPolicyControl()) 161 - ->setName('viewPolicy') 162 - ->setPolicyObject($countdown) 163 - ->setPolicies($policies) 164 - ->setSpacePHID($v_space) 165 - ->setValue($v_view) 166 - ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) 167 - ->appendControl( 168 - id(new AphrontFormPolicyControl()) 169 - ->setName('editPolicy') 170 - ->setPolicyObject($countdown) 171 - ->setPolicies($policies) 172 - ->setValue($v_edit) 173 - ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)) 174 - ->appendControl( 175 - id(new AphrontFormTokenizerControl()) 176 - ->setLabel(pht('Projects')) 177 - ->setName('projects') 178 - ->setValue($v_projects) 179 - ->setDatasource(new PhabricatorProjectDatasource())) 180 - ->appendChild( 181 - id(new AphrontFormSubmitControl()) 182 - ->addCancelButton($cancel_uri) 183 - ->setValue($submit_label)); 184 - 185 - $form_box = id(new PHUIObjectBoxView()) 186 - ->setHeaderText(pht('Countdown')) 187 - ->setFormErrors($errors) 188 - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 189 - ->setForm($form); 190 - 191 - $header = id(new PHUIHeaderView()) 192 - ->setHeader($title) 193 - ->setHeaderIcon($header_icon); 194 - 195 - $view = id(new PHUITwoColumnView()) 196 - ->setHeader($header) 197 - ->setFooter($form_box); 198 - 199 - return $this->newPage() 200 - ->setTitle($title) 201 - ->setCrumbs($crumbs) 202 - ->appendChild( 203 - array( 204 - $view, 205 - )); 7 + return id(new PhabricatorCountdownEditEngine()) 8 + ->setController($this) 9 + ->buildResponse(); 206 10 } 207 11 208 12 }
+10
src/applications/countdown/controller/PhabricatorCountdownListController.php
··· 13 13 ->buildResponse(); 14 14 } 15 15 16 + protected function buildApplicationCrumbs() { 17 + $crumbs = parent::buildApplicationCrumbs(); 18 + 19 + id(new PhabricatorCountdownEditEngine()) 20 + ->setViewer($this->getViewer()) 21 + ->addActionToCrumbs($crumbs); 22 + 23 + return $crumbs; 24 + } 25 + 16 26 }
+5 -23
src/applications/countdown/controller/PhabricatorCountdownViewController.php
··· 55 55 $timeline = $this->buildTransactionTimeline( 56 56 $countdown, 57 57 new PhabricatorCountdownTransactionQuery()); 58 - $add_comment = $this->buildCommentForm($countdown); 58 + 59 + $comment_view = id(new PhabricatorCountdownEditEngine()) 60 + ->setViewer($viewer) 61 + ->buildEditEngineCommentView($countdown); 59 62 60 63 $content = array( 61 64 $countdown_view, 62 65 $timeline, 63 - $add_comment, 66 + $comment_view, 64 67 ); 65 68 66 69 $view = id(new PHUITwoColumnView()) ··· 133 136 ->setImage($image_uri) 134 137 ->setImageHref($image_href) 135 138 ->setContent($content); 136 - } 137 - 138 - private function buildCommentForm(PhabricatorCountdown $countdown) { 139 - $viewer = $this->getViewer(); 140 - 141 - $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); 142 - 143 - $add_comment_header = $is_serious 144 - ? pht('Add Comment') 145 - : pht('Last Words'); 146 - 147 - $draft = PhabricatorDraft::newFromUserAndKey( 148 - $viewer, $countdown->getPHID()); 149 - 150 - return id(new PhabricatorApplicationTransactionCommentView()) 151 - ->setUser($viewer) 152 - ->setObjectPHID($countdown->getPHID()) 153 - ->setDraft($draft) 154 - ->setHeaderText($add_comment_header) 155 - ->setAction($this->getApplicationURI('/comment/'.$countdown->getID().'/')) 156 - ->setSubmitButtonName(pht('Add Comment')); 157 139 } 158 140 159 141 }
+108
src/applications/countdown/editor/PhabricatorCountdownEditEngine.php
··· 1 + <?php 2 + 3 + final class PhabricatorCountdownEditEngine 4 + extends PhabricatorEditEngine { 5 + 6 + const ENGINECONST = 'countdown.countdown'; 7 + 8 + public function isEngineConfigurable() { 9 + return false; 10 + } 11 + 12 + public function getEngineName() { 13 + return pht('Countdowns'); 14 + } 15 + 16 + public function getSummaryHeader() { 17 + return pht('Edit Countdowns'); 18 + } 19 + 20 + public function getSummaryText() { 21 + return pht('Creates and edits countdowns.'); 22 + } 23 + 24 + public function getEngineApplicationClass() { 25 + return 'PhabricatorCountdownApplication'; 26 + } 27 + 28 + protected function newEditableObject() { 29 + return PhabricatorCountdown::initializeNewCountdown( 30 + $this->getViewer()); 31 + } 32 + 33 + protected function newObjectQuery() { 34 + return id(new PhabricatorCountdownQuery()); 35 + } 36 + 37 + protected function getObjectCreateTitleText($object) { 38 + return pht('Create Countdown'); 39 + } 40 + 41 + protected function getObjectCreateButtonText($object) { 42 + return pht('Create Countdown'); 43 + } 44 + 45 + protected function getObjectEditTitleText($object) { 46 + return pht('Edit Countdown: %s', $object->getTitle()); 47 + } 48 + 49 + protected function getObjectEditShortText($object) { 50 + return pht('Edit Countdown'); 51 + } 52 + 53 + protected function getObjectCreateShortText() { 54 + return pht('Create Countdown'); 55 + } 56 + 57 + protected function getObjectName() { 58 + return pht('Countdown'); 59 + } 60 + 61 + protected function getCommentViewHeaderText($object) { 62 + return pht('Last Words'); 63 + } 64 + 65 + protected function getCommentViewButtonText($object) { 66 + return pht('Contemplate Infinity'); 67 + } 68 + 69 + protected function getObjectViewURI($object) { 70 + return $object->getURI(); 71 + } 72 + 73 + protected function buildCustomEditFields($object) { 74 + $epoch_value = $object->getEpoch(); 75 + if ($epoch_value === null) { 76 + $epoch_value = PhabricatorTime::getNow(); 77 + } 78 + 79 + return array( 80 + id(new PhabricatorTextEditField()) 81 + ->setKey('name') 82 + ->setLabel(pht('Name')) 83 + ->setIsRequired(true) 84 + ->setTransactionType(PhabricatorCountdownTransaction::TYPE_TITLE) 85 + ->setDescription(pht('The countdown name.')) 86 + ->setConduitDescription(pht('Rename the countdown.')) 87 + ->setConduitTypeDescription(pht('New countdown name.')) 88 + ->setValue($object->getTitle()), 89 + id(new PhabricatorEpochEditField()) 90 + ->setKey('epoch') 91 + ->setLabel(pht('End Date')) 92 + ->setTransactionType(PhabricatorCountdownTransaction::TYPE_EPOCH) 93 + ->setDescription(pht('Date when the countdown ends.')) 94 + ->setConduitDescription(pht('Change the end date of the countdown.')) 95 + ->setConduitTypeDescription(pht('New countdown end date.')) 96 + ->setValue($epoch_value), 97 + id(new PhabricatorRemarkupEditField()) 98 + ->setKey('description') 99 + ->setLabel(pht('Description')) 100 + ->setTransactionType(PhabricatorCountdownTransaction::TYPE_DESCRIPTION) 101 + ->setDescription(pht('Description of the countdown.')) 102 + ->setConduitDescription(pht('Change the countdown description.')) 103 + ->setConduitTypeDescription(pht('New description.')) 104 + ->setValue($object->getDescription()), 105 + ); 106 + } 107 + 108 + }
+16 -8
src/applications/countdown/editor/PhabricatorCountdownEditor.php
··· 120 120 } 121 121 break; 122 122 case PhabricatorCountdownTransaction::TYPE_EPOCH: 123 - $date_value = AphrontFormDateControlValue::newFromEpoch( 124 - $this->requireActor(), 125 - $object->getEpoch()); 126 - if (!$date_value->isValid()) { 123 + if (!$object->getEpoch() && !$xactions) { 127 124 $error = new PhabricatorApplicationTransactionValidationError( 128 125 $type, 129 - pht('Invalid'), 130 - pht('You must give the countdown a valid end date.'), 131 - nonempty(last($xactions), null)); 132 - 126 + pht('Required'), 127 + pht('You must give the countdown an end date.'), 128 + null); 133 129 $error->setIsMissingFieldError(true); 134 130 $errors[] = $error; 131 + } 132 + 133 + foreach ($xactions as $xaction) { 134 + $value = $xaction->getNewValue(); 135 + if (!$value->isValid()) { 136 + $error = new PhabricatorApplicationTransactionValidationError( 137 + $type, 138 + pht('Invalid'), 139 + pht('You must give the countdown a valid end date.'), 140 + $xaction); 141 + $errors[] = $error; 142 + } 135 143 } 136 144 break; 137 145 }
+8 -1
src/applications/countdown/storage/PhabricatorCountdown.php
··· 28 28 $view_policy = $app->getPolicy( 29 29 PhabricatorCountdownDefaultViewCapability::CAPABILITY); 30 30 31 + $edit_policy = $app->getPolicy( 32 + PhabricatorCountdownDefaultEditCapability::CAPABILITY); 33 + 31 34 return id(new PhabricatorCountdown()) 32 35 ->setAuthorPHID($actor->getPHID()) 33 36 ->setViewPolicy($view_policy) 34 - ->setEpoch(PhabricatorTime::getNow()) 37 + ->setEditPolicy($edit_policy) 35 38 ->setSpacePHID($actor->getDefaultSpacePHID()); 36 39 } 37 40 ··· 53 56 54 57 public function getMonogram() { 55 58 return 'C'.$this->getID(); 59 + } 60 + 61 + public function getURI() { 62 + return '/'.$this->getMonogram(); 56 63 } 57 64 58 65 public function save() {
+24 -82
src/applications/countdown/storage/PhabricatorCountdownTransaction.php
··· 33 33 $type = $this->getTransactionType(); 34 34 switch ($type) { 35 35 case self::TYPE_TITLE: 36 - if ($old === null) { 37 - return pht( 38 - '%s created this countdown.', 39 - $this->renderHandleLink($author_phid)); 40 - } else { 41 - return pht( 42 - '%s renamed this countdown from "%s" to "%s".', 43 - $this->renderHandleLink($author_phid), 44 - $old, 45 - $new); 46 - } 47 - break; 36 + return pht( 37 + '%s renamed this countdown from "%s" to "%s".', 38 + $this->renderHandleLink($author_phid), 39 + $old, 40 + $new); 48 41 case self::TYPE_DESCRIPTION: 49 - if ($old === null) { 50 - return pht( 51 - '%s set the description of this countdown.', 52 - $this->renderHandleLink($author_phid)); 53 - } else { 54 - return pht( 55 - '%s edited the description of this countdown.', 56 - $this->renderHandleLink($author_phid)); 57 - } 58 - break; 42 + return pht( 43 + '%s edited the description of this countdown.', 44 + $this->renderHandleLink($author_phid)); 59 45 case self::TYPE_EPOCH: 60 - if ($old === null) { 61 - return pht( 62 - '%s set this countdown to end on %s.', 63 - $this->renderHandleLink($author_phid), 64 - phabricator_datetime($new, $this->getViewer())); 65 - } else if ($old != $new) { 66 - return pht( 67 - '%s updated this countdown to end on %s.', 68 - $this->renderHandleLink($author_phid), 69 - phabricator_datetime($new, $this->getViewer())); 70 - } 71 - break; 46 + return pht( 47 + '%s updated this countdown to end on %s.', 48 + $this->renderHandleLink($author_phid), 49 + phabricator_datetime($new, $this->getViewer())); 72 50 } 73 51 74 52 return parent::getTitle(); ··· 84 62 $type = $this->getTransactionType(); 85 63 switch ($type) { 86 64 case self::TYPE_TITLE: 87 - if ($old === null) { 88 - return pht( 89 - '%s created %s.', 90 - $this->renderHandleLink($author_phid), 91 - $this->renderHandleLink($object_phid)); 92 - 93 - } else { 94 - return pht( 95 - '%s renamed %s.', 96 - $this->renderHandleLink($author_phid), 97 - $this->renderHandleLink($object_phid)); 98 - } 99 - break; 65 + return pht( 66 + '%s renamed %s.', 67 + $this->renderHandleLink($author_phid), 68 + $this->renderHandleLink($object_phid)); 100 69 case self::TYPE_DESCRIPTION: 101 - if ($old === null) { 102 - return pht( 103 - '%s set the description of %s.', 104 - $this->renderHandleLink($author_phid), 105 - $this->renderHandleLink($object_phid)); 106 - 107 - } else { 108 - return pht( 109 - '%s edited the description of %s.', 110 - $this->renderHandleLink($author_phid), 111 - $this->renderHandleLink($object_phid)); 112 - } 113 - break; 70 + return pht( 71 + '%s edited the description of %s.', 72 + $this->renderHandleLink($author_phid), 73 + $this->renderHandleLink($object_phid)); 114 74 case self::TYPE_EPOCH: 115 - if ($old === null) { 116 - return pht( 117 - '%s set the end date of %s.', 118 - $this->renderHandleLink($author_phid), 119 - $this->renderHandleLink($object_phid)); 120 - 121 - } else { 122 - return pht( 123 - '%s edited the end date of %s.', 124 - $this->renderHandleLink($author_phid), 125 - $this->renderHandleLink($object_phid)); 126 - } 127 - break; 75 + return pht( 76 + '%s edited the end date of %s.', 77 + $this->renderHandleLink($author_phid), 78 + $this->renderHandleLink($object_phid)); 128 79 } 129 80 130 81 return parent::getTitleForFeed(); ··· 148 99 } 149 100 150 101 return $tags; 151 - } 152 - 153 - public function shouldHide() { 154 - $old = $this->getOldValue(); 155 - switch ($this->getTransactionType()) { 156 - case self::TYPE_DESCRIPTION: 157 - return ($old === null); 158 - } 159 - return parent::shouldHide(); 160 102 } 161 103 162 104 public function hasChangeDetails() {
+21
src/applications/transactions/editfield/PhabricatorEpochEditField.php
··· 1 + <?php 2 + 3 + final class PhabricatorEpochEditField 4 + extends PhabricatorEditField { 5 + 6 + protected function newControl() { 7 + return id(new AphrontFormDateControl()) 8 + ->setViewer($this->getViewer()); 9 + } 10 + 11 + protected function newHTTPParameterType() { 12 + return new AphrontEpochHTTPParameterType(); 13 + } 14 + 15 + protected function newConduitParameterType() { 16 + // TODO: This isn't correct, but we don't have any methods which use this 17 + // yet. 18 + return new ConduitIntParameterType(); 19 + } 20 + 21 + }
+6 -3
src/view/form/control/AphrontFormDateControl.php
··· 130 130 $date_format = $this->getDateFormat(); 131 131 $timezone = $this->getTimezone(); 132 132 133 - $datetime = new DateTime($this->valueDate, $timezone); 134 - $date = $datetime->format($date_format); 133 + try { 134 + $datetime = new DateTime($this->valueDate, $timezone); 135 + } catch (Exception $ex) { 136 + return $this->valueDate; 137 + } 135 138 136 - return $date; 139 + return $datetime->format($date_format); 137 140 } 138 141 139 142 private function getTimeFormat() {
+121 -61
src/view/form/control/AphrontFormDateControlValue.php
··· 84 84 $value = new AphrontFormDateControlValue(); 85 85 $value->viewer = $request->getViewer(); 86 86 87 - list($value->valueDate, $value->valueTime) = 88 - $value->getFormattedDateFromDate( 89 - $request->getStr($key.'_d'), 90 - $request->getStr($key.'_t')); 87 + $datetime = $request->getStr($key); 88 + if (strlen($datetime)) { 89 + $date = $datetime; 90 + $time = null; 91 + } else { 92 + $date = $request->getStr($key.'_d'); 93 + $time = $request->getStr($key.'_t'); 94 + } 95 + 96 + // If this looks like an epoch timestamp, prefix it with "@" so that 97 + // DateTime() reads it as one. Assume small numbers are a "Ymd" digit 98 + // string instead of an epoch timestamp for a time in 1970. 99 + if (ctype_digit($date) && ($date > 30000000)) { 100 + $date = '@'.$date; 101 + $time = null; 102 + } 103 + 104 + $value->valueDate = $date; 105 + $value->valueTime = $time; 106 + 107 + $formatted = $value->getFormattedDateFromDate( 108 + $value->valueDate, 109 + $value->valueTime); 110 + 111 + if ($formatted) { 112 + list($value->valueDate, $value->valueTime) = $formatted; 113 + } 91 114 92 115 $value->valueEnabled = $request->getStr($key.'_e'); 93 116 return $value; ··· 96 119 public static function newFromEpoch(PhabricatorUser $viewer, $epoch) { 97 120 $value = new AphrontFormDateControlValue(); 98 121 $value->viewer = $viewer; 122 + 123 + if (!$epoch) { 124 + return $value; 125 + } 126 + 99 127 $readable = $value->formatTime($epoch, 'Y!m!d!g:i A'); 100 128 $readable = explode('!', $readable, 4); 101 129 ··· 120 148 $value = new AphrontFormDateControlValue(); 121 149 $value->viewer = $viewer; 122 150 123 - list($value->valueDate, $value->valueTime) = 124 - $value->getFormattedDateFromDate( 125 - idx($dictionary, 'd'), 126 - idx($dictionary, 't')); 151 + $value->valueDate = idx($dictionary, 'd'); 152 + $value->valueTime = idx($dictionary, 't'); 153 + 154 + $formatted = $value->getFormattedDateFromDate( 155 + $value->valueDate, 156 + $value->valueTime); 157 + 158 + if ($formatted) { 159 + list($value->valueDate, $value->valueTime) = $formatted; 160 + } 127 161 128 162 $value->valueEnabled = idx($dictionary, 'e'); 129 163 ··· 170 204 return null; 171 205 } 172 206 173 - $date = $this->valueDate; 174 - $time = $this->valueTime; 175 - $zone = $this->getTimezone(); 176 - 177 - if (!strlen($time)) { 207 + $datetime = $this->newDateTime($this->valueDate, $this->valueTime); 208 + if (!$datetime) { 178 209 return null; 179 210 } 180 211 181 - $colloquial = array( 182 - 'elevenses' => '11:00 AM', 183 - 'morning tea' => '11:00 AM', 184 - 'noon' => '12:00 PM', 185 - 'high noon' => '12:00 PM', 186 - 'lunch' => '12:00 PM', 187 - 'tea time' => '3:00 PM', 188 - 'witching hour' => '12:00 AM', 189 - 'midnight' => '12:00 AM', 190 - ); 191 - 192 - $normalized = phutil_utf8_strtolower($time); 193 - if (isset($colloquial[$normalized])) { 194 - $time = $colloquial[$normalized]; 195 - } 196 - 197 - try { 198 - $datetime = new DateTime("{$date} {$time}", $zone); 199 - $value = $datetime->format('U'); 200 - } catch (Exception $ex) { 201 - $value = null; 202 - } 203 - return $value; 212 + return $datetime->format('U'); 204 213 } 205 214 206 215 private function getTimeFormat() { ··· 214 223 } 215 224 216 225 private function getFormattedDateFromDate($date, $time) { 217 - $original_input = $date; 218 - $zone = $this->getTimezone(); 219 - $separator = $this->getFormatSeparator(); 220 - $parts = preg_split('@[,./:-]@', $date); 221 - $date = implode($separator, $parts); 222 - $date = id(new DateTime($date, $zone)); 223 - 224 - if ($date) { 225 - $date = $date->format($this->getDateFormat()); 226 - } else { 227 - $date = $original_input; 226 + $datetime = $this->newDateTime($date, $time); 227 + if (!$datetime) { 228 + return null; 228 229 } 229 - 230 - $date = id(new DateTime("{$date} {$time}", $zone)); 231 230 232 231 return array( 233 - $date->format($this->getDateFormat()), 234 - $date->format($this->getTimeFormat()), 232 + $datetime->format($this->getDateFormat()), 233 + $datetime->format($this->getTimeFormat()), 235 234 ); 235 + 236 + return array($date, $time); 237 + } 238 + 239 + private function newDateTime($date, $time) { 240 + $date = $this->getStandardDateFormat($date); 241 + $time = $this->getStandardTimeFormat($time); 242 + try { 243 + $datetime = new DateTime("{$date} {$time}"); 244 + } catch (Exception $ex) { 245 + return null; 246 + } 247 + 248 + // Set the timezone explicitly because it is ignored in the constructor 249 + // if the date is an epoch timestamp. 250 + $zone = $this->getTimezone(); 251 + $datetime->setTimezone($zone); 252 + 253 + return $datetime; 236 254 } 237 255 238 256 private function getFormattedDateFromParts( ··· 261 279 } 262 280 263 281 public function getDateTime() { 264 - $epoch = $this->getEpoch(); 265 - $date = null; 266 - 267 - if ($epoch) { 268 - $zone = $this->getTimezone(); 269 - $date = new DateTime('@'.$epoch); 270 - $date->setTimeZone($zone); 271 - } 272 - 273 - return $date; 282 + return $this->newDateTime(); 274 283 } 275 284 276 285 private function getTimezone() { ··· 283 292 return $this->zone; 284 293 } 285 294 295 + private function getStandardDateFormat($date) { 296 + $colloquial = array( 297 + 'newyear' => 'January 1', 298 + 'valentine' => 'February 14', 299 + 'pi' => 'March 14', 300 + 'christma' => 'December 25', 301 + ); 302 + 303 + // Lowercase the input, then remove punctuation, a "day" suffix, and an 304 + // "s" if one is present. This allows all of these to match. This allows 305 + // variations like "New Year's Day" and "New Year" to both match. 306 + $normalized = phutil_utf8_strtolower($date); 307 + $normalized = preg_replace('/[^a-z]/', '', $normalized); 308 + $normalized = preg_replace('/day\z/', '', $normalized); 309 + $normalized = preg_replace('/s\z/', '', $normalized); 310 + 311 + if (isset($colloquial[$normalized])) { 312 + return $colloquial[$normalized]; 313 + } 314 + 315 + $separator = $this->getFormatSeparator(); 316 + $parts = preg_split('@[,./:-]@', $date); 317 + return implode($separator, $parts); 318 + } 319 + 320 + private function getStandardTimeFormat($time) { 321 + $colloquial = array( 322 + 'crack of dawn' => '5:00 AM', 323 + 'dawn' => '6:00 AM', 324 + 'early' => '7:00 AM', 325 + 'morning' => '8:00 AM', 326 + 'elevenses' => '11:00 AM', 327 + 'morning tea' => '11:00 AM', 328 + 'noon' => '12:00 PM', 329 + 'high noon' => '12:00 PM', 330 + 'lunch' => '12:00 PM', 331 + 'afternoon' => '2:00 PM', 332 + 'tea time' => '3:00 PM', 333 + 'evening' => '7:00 PM', 334 + 'late' => '11:00 PM', 335 + 'witching hour' => '12:00 AM', 336 + 'midnight' => '12:00 AM', 337 + ); 338 + 339 + $normalized = phutil_utf8_strtolower($time); 340 + if (isset($colloquial[$normalized])) { 341 + $time = $colloquial[$normalized]; 342 + } 343 + 344 + return $time; 345 + } 286 346 287 347 }