@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.

Create a status tool by giving /calendar/ some teeth

Summary: you can now add, edit, and delete status events. also added a "description" to status events and surface it in the big calendar view on mouse hover. some refactoring changes as well to make validation logic centralized within the storage class.

Test Plan: added, edited, deleted. yay.

Reviewers: epriestley, vrana

Reviewed By: epriestley

CC: aran, Korvin

Maniphest Tasks: T407

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

+718 -80
+2
resources/sql/patches/statustxt.sql
··· 1 + ALTER TABLE {$NAMESPACE}_user.user_status 2 + ADD description LONGTEXT NOT NULL COLLATE utf8_bin;
+17 -1
src/__phutil_library_map__.php
··· 553 553 'PhabricatorApplicationApplications' => 'applications/meta/application/PhabricatorApplicationApplications.php', 554 554 'PhabricatorApplicationAudit' => 'applications/audit/application/PhabricatorApplicationAudit.php', 555 555 'PhabricatorApplicationAuth' => 'applications/auth/application/PhabricatorApplicationAuth.php', 556 + 'PhabricatorApplicationCalendar' => 'applications/calendar/application/PhabricatorApplicationCalendar.php', 556 557 'PhabricatorApplicationConduit' => 'applications/conduit/application/PhabricatorApplicationConduit.php', 557 558 'PhabricatorApplicationCountdown' => 'applications/countdown/application/PhabricatorApplicationCountdown.php', 558 559 'PhabricatorApplicationDaemons' => 'applications/daemon/application/PhabricatorApplicationDaemons.php', ··· 609 610 'PhabricatorCalendarBrowseController' => 'applications/calendar/controller/PhabricatorCalendarBrowseController.php', 610 611 'PhabricatorCalendarController' => 'applications/calendar/controller/PhabricatorCalendarController.php', 611 612 'PhabricatorCalendarDAO' => 'applications/calendar/storage/PhabricatorCalendarDAO.php', 613 + 'PhabricatorCalendarDeleteStatusController' => 'applications/calendar/controller/PhabricatorCalendarDeleteStatusController.php', 614 + 'PhabricatorCalendarEditStatusController' => 'applications/calendar/controller/PhabricatorCalendarEditStatusController.php', 612 615 'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php', 613 616 'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php', 617 + 'PhabricatorCalendarViewStatusController' => 'applications/calendar/controller/PhabricatorCalendarViewStatusController.php', 614 618 'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php', 615 619 'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php', 616 620 'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/PhabricatorChatLogChannelLogController.php', ··· 1125 1129 'PhabricatorUserProfile' => 'applications/people/storage/PhabricatorUserProfile.php', 1126 1130 'PhabricatorUserSSHKey' => 'applications/settings/storage/PhabricatorUserSSHKey.php', 1127 1131 'PhabricatorUserStatus' => 'applications/people/storage/PhabricatorUserStatus.php', 1132 + 'PhabricatorUserStatusInvalidEpochException' => 'applications/people/exception/PhabricatorUserStatusInvalidEpochException.php', 1133 + 'PhabricatorUserStatusOverlapException' => 'applications/people/exception/PhabricatorUserStatusOverlapException.php', 1128 1134 'PhabricatorUserTestCase' => 'applications/people/storage/__tests__/PhabricatorUserTestCase.php', 1129 1135 'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php', 1130 1136 'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php', ··· 1748 1754 'PhabricatorApplicationApplications' => 'PhabricatorApplication', 1749 1755 'PhabricatorApplicationAudit' => 'PhabricatorApplication', 1750 1756 'PhabricatorApplicationAuth' => 'PhabricatorApplication', 1757 + 'PhabricatorApplicationCalendar' => 'PhabricatorApplication', 1751 1758 'PhabricatorApplicationConduit' => 'PhabricatorApplication', 1752 1759 'PhabricatorApplicationCountdown' => 'PhabricatorApplication', 1753 1760 'PhabricatorApplicationDaemons' => 'PhabricatorApplication', ··· 1780 1787 'PhabricatorApplicationUIExamples' => 'PhabricatorApplication', 1781 1788 'PhabricatorApplicationsListController' => 'PhabricatorController', 1782 1789 'PhabricatorAuditAddCommentController' => 'PhabricatorAuditController', 1783 - 'PhabricatorAuditComment' => 'PhabricatorAuditDAO', 1790 + 'PhabricatorAuditComment' => 1791 + array( 1792 + 0 => 'PhabricatorAuditDAO', 1793 + 1 => 'PhabricatorMarkupInterface', 1794 + ), 1784 1795 'PhabricatorAuditCommentEditor' => 'PhabricatorEditor', 1785 1796 'PhabricatorAuditCommitListView' => 'AphrontView', 1786 1797 'PhabricatorAuditController' => 'PhabricatorController', ··· 1803 1814 'PhabricatorCalendarBrowseController' => 'PhabricatorCalendarController', 1804 1815 'PhabricatorCalendarController' => 'PhabricatorController', 1805 1816 'PhabricatorCalendarDAO' => 'PhabricatorLiskDAO', 1817 + 'PhabricatorCalendarDeleteStatusController' => 'PhabricatorCalendarController', 1818 + 'PhabricatorCalendarEditStatusController' => 'PhabricatorCalendarController', 1806 1819 'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO', 1807 1820 'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase', 1821 + 'PhabricatorCalendarViewStatusController' => 'PhabricatorCalendarController', 1808 1822 'PhabricatorChangesetResponse' => 'AphrontProxyResponse', 1809 1823 'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController', 1810 1824 'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController', ··· 2265 2279 'PhabricatorUserProfile' => 'PhabricatorUserDAO', 2266 2280 'PhabricatorUserSSHKey' => 'PhabricatorUserDAO', 2267 2281 'PhabricatorUserStatus' => 'PhabricatorUserDAO', 2282 + 'PhabricatorUserStatusInvalidEpochException' => 'Exception', 2283 + 'PhabricatorUserStatusOverlapException' => 'Exception', 2268 2284 'PhabricatorUserTestCase' => 'PhabricatorTestCase', 2269 2285 'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO', 2270 2286 'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO',
-4
src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
··· 113 113 'keyboardshortcut/' => 'PhabricatorHelpKeyboardShortcutController', 114 114 ), 115 115 116 - '/calendar/' => array( 117 - '' => 'PhabricatorCalendarBrowseController', 118 - ), 119 - 120 116 '/drydock/' => array( 121 117 '' => 'DrydockResourceListController', 122 118 'resource/' => 'DrydockResourceListController',
+58
src/applications/calendar/application/PhabricatorApplicationCalendar.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorApplicationCalendar extends PhabricatorApplication { 20 + 21 + public function getShortDescription() { 22 + return pht('Dates and Stuff'); 23 + } 24 + 25 + public function getFlavorText() { 26 + return pht('Never miss an episode ever again.'); 27 + } 28 + 29 + public function getBaseURI() { 30 + return '/calendar/'; 31 + } 32 + 33 + public function getTitleGlyph() { 34 + // Unicode has a calendar character but it's in some distant code plane, 35 + // use "keyboard" since it looks vaguely similar. 36 + return "\xE2\x8C\xA8"; 37 + } 38 + 39 + public function getRoutes() { 40 + return array( 41 + '/calendar/' => array( 42 + '' => 'PhabricatorCalendarBrowseController', 43 + 'status/' => array( 44 + '' => 'PhabricatorCalendarViewStatusController', 45 + 'create/' => 46 + 'PhabricatorCalendarEditStatusController', 47 + 'delete/(?P<id>[1-9]\d*)/' => 48 + 'PhabricatorCalendarDeleteStatusController', 49 + 'edit/(?P<id>[1-9]\d*)/' => 50 + 'PhabricatorCalendarEditStatusController', 51 + 'view/(?P<phid>[^/]+)/' => 52 + 'PhabricatorCalendarViewStatusController', 53 + ), 54 + ), 55 + ); 56 + } 57 + 58 + }
+46 -9
src/applications/calendar/controller/PhabricatorCalendarBrowseController.php
··· 20 20 extends PhabricatorCalendarController { 21 21 22 22 public function processRequest() { 23 + $now = time(); 23 24 $request = $this->getRequest(); 24 - $user = $request->getUser(); 25 - 26 - // TODO: These should be user-based and navigable in the interface. 27 - $year = idate('Y'); 28 - $month = idate('m'); 25 + $user = $request->getUser(); 26 + $year_d = phabricator_format_local_time($now, $user, 'Y'); 27 + $year = $request->getInt('year', $year_d); 28 + $month_d = phabricator_format_local_time($now, $user, 'm'); 29 + $month = $request->getInt('month', $month_d); 29 30 30 31 $holidays = id(new PhabricatorCalendarHoliday())->loadAllWhere( 31 32 'day BETWEEN %s AND %s', ··· 39 40 strtotime("{$year}-{$month}-01 next month")); 40 41 41 42 $month_view = new AphrontCalendarMonthView($month, $year); 43 + $month_view->setBrowseURI($request->getRequestURI()); 42 44 $month_view->setUser($user); 43 45 $month_view->setHolidays($holidays); 44 46 ··· 53 55 $status_text = $status->getTextStatus(); 54 56 $event->setUserPHID($status->getUserPHID()); 55 57 $event->setName("{$name_text} ({$status_text})"); 56 - $event->setDescription($status->getStatusDescription($user)); 58 + $details = ''; 59 + if ($status->getDescription()) { 60 + $details = "\n\n".rtrim(phutil_escape_html($status->getDescription())); 61 + } 62 + $event->setDescription( 63 + $status->getTerseSummary($user).$details 64 + ); 57 65 $month_view->addEvent($event); 58 66 } 59 67 60 - return $this->buildStandardPageResponse( 68 + $nav = $this->buildSideNavView(); 69 + $nav->selectFilter('edit'); 70 + $nav->appendChild( 61 71 array( 72 + $this->getNoticeView(), 62 73 '<div style="padding: 2em;">', 63 74 $month_view, 64 75 '</div>', 65 - ), 66 - array( 76 + )); 77 + 78 + return $this->buildApplicationPage( 79 + $nav, 80 + array( 67 81 'title' => 'Calendar', 82 + 'device' => true, 68 83 )); 69 84 } 85 + 86 + private function getNoticeView() { 87 + $request = $this->getRequest(); 88 + $view = null; 89 + 90 + if ($request->getExists('created')) { 91 + $view = id(new AphrontErrorView()) 92 + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) 93 + ->setTitle(pht('Successfully created your status.')); 94 + } else if ($request->getExists('updated')) { 95 + $view = id(new AphrontErrorView()) 96 + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) 97 + ->setTitle(pht('Successfully updated your status.')); 98 + } else if ($request->getExists('deleted')) { 99 + $view = id(new AphrontErrorView()) 100 + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) 101 + ->setTitle(pht('Successfully deleted your status.')); 102 + } 103 + 104 + return $view; 105 + } 106 + 70 107 }
+16 -11
src/applications/calendar/controller/PhabricatorCalendarController.php
··· 18 18 19 19 abstract class PhabricatorCalendarController extends PhabricatorController { 20 20 21 - public function buildStandardPageResponse($view, array $data) { 22 21 23 - $page = $this->buildStandardPageView(); 22 + protected function buildSideNavView(PhabricatorUserStatus $status = null) { 23 + $nav = new AphrontSideNavFilterView(); 24 + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); 24 25 25 - $page->setApplicationName('Calendar'); 26 - $page->setBaseURI('/calendar/'); 27 - $page->setTitle(idx($data, 'title')); 26 + $nav->addFilter('', pht('Calendar'), $this->getApplicationURI()); 27 + 28 + $nav->addSpacer(); 29 + 30 + $nav->addLabel(pht('Create Events')); 31 + $nav->addFilter('status/create/', pht('New Status')); 28 32 29 - // Unicode has a calendar character but it's in some distant code plane, 30 - // use "keyboard" since it looks vaguely similar. 31 - $page->setGlyph("\xE2\x8C\xA8"); 32 - $page->appendChild($view); 33 + $nav->addSpacer(); 34 + $nav->addLabel(pht('Your Events')); 35 + if ($status && $status->getID()) { 36 + $nav->addFilter('status/edit/'.$status->getID().'/', pht('Edit Status')); 37 + } 38 + $nav->addFilter('status/', pht('Upcoming Statuses')); 33 39 34 - $response = new AphrontWebpageResponse(); 35 - return $response->setContent($page->render()); 40 + return $nav; 36 41 } 37 42 38 43 }
+70
src/applications/calendar/controller/PhabricatorCalendarDeleteStatusController.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorCalendarDeleteStatusController 20 + extends PhabricatorCalendarController { 21 + 22 + private $id; 23 + 24 + public function willProcessRequest(array $data) { 25 + $this->id = idx($data, 'id'); 26 + } 27 + 28 + public function processRequest() { 29 + $request = $this->getRequest(); 30 + $user = $request->getUser(); 31 + $status = id(new PhabricatorUserStatus()) 32 + ->loadOneWhere('id = %d', $this->id); 33 + 34 + if (!$status) { 35 + return new Aphront404Response(); 36 + } 37 + if ($status->getUserPHID() != $user->getPHID()) { 38 + return new Aphront403Response(); 39 + } 40 + 41 + if ($request->isFormPost()) { 42 + $status->delete(); 43 + $uri = new PhutilURI($this->getApplicationURI()); 44 + $uri->setQueryParams( 45 + array( 46 + 'deleted' => true, 47 + ) 48 + ); 49 + return id(new AphrontRedirectResponse()) 50 + ->setURI($uri); 51 + } 52 + 53 + $dialog = new AphrontDialogView(); 54 + $dialog->setUser($user); 55 + $dialog->setTitle(pht('Really delete status?')); 56 + $dialog->appendChild(phutil_render_tag( 57 + 'p', 58 + array(), 59 + pht('Permanently delete this status? This action can not be undone.') 60 + )); 61 + $dialog->addSubmitButton(pht('Delete')); 62 + $dialog->addCancelButton( 63 + $this->getApplicationURI('status/edit/'.$status->getID().'/') 64 + ); 65 + 66 + return id(new AphrontDialogResponse())->setDialog($dialog); 67 + 68 + } 69 + 70 + }
+168
src/applications/calendar/controller/PhabricatorCalendarEditStatusController.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorCalendarEditStatusController 20 + extends PhabricatorCalendarController { 21 + 22 + private $id; 23 + 24 + public function willProcessRequest(array $data) { 25 + $this->id = idx($data, 'id'); 26 + } 27 + 28 + public function isCreate() { 29 + return !$this->id; 30 + } 31 + 32 + public function processRequest() { 33 + $request = $this->getRequest(); 34 + $user = $request->getUser(); 35 + 36 + $start_time = id(new AphrontFormDateControl()) 37 + ->setUser($user) 38 + ->setName('start') 39 + ->setLabel(pht('Start')) 40 + ->setInitialTime(AphrontFormDateControl::TIME_START_OF_DAY); 41 + 42 + $end_time = id(new AphrontFormDateControl()) 43 + ->setUser($user) 44 + ->setName('end') 45 + ->setLabel(pht('End')) 46 + ->setInitialTime(AphrontFormDateControl::TIME_END_OF_DAY); 47 + 48 + if ($this->isCreate()) { 49 + $status = new PhabricatorUserStatus(); 50 + $end_value = $end_time->readValueFromRequest($request); 51 + $start_value = $start_time->readValueFromRequest($request); 52 + $submit_label = pht('Create'); 53 + $filter = 'status/create/'; 54 + $page_title = pht('Create Status'); 55 + $redirect = 'created'; 56 + } else { 57 + $status = id(new PhabricatorUserStatus()) 58 + ->loadOneWhere('id = %d', $this->id); 59 + $end_time->setValue($status->getDateTo()); 60 + $start_time->setValue($status->getDateFrom()); 61 + $submit_label = pht('Update'); 62 + $filter = 'status/edit/'.$status->getID().'/'; 63 + $page_title = pht('Update Status'); 64 + $redirect = 'updated'; 65 + 66 + if ($status->getUserPHID() != $user->getPHID()) { 67 + return new Aphront403Response(); 68 + } 69 + } 70 + 71 + $errors = array(); 72 + if ($request->isFormPost()) { 73 + $type = $request->getInt('status'); 74 + $start_value = $start_time->readValueFromRequest($request); 75 + $end_value = $end_time->readValueFromRequest($request); 76 + $description = $request->getStr('description'); 77 + 78 + try { 79 + $status 80 + ->setUserPHID($user->getPHID()) 81 + ->setStatus($type) 82 + ->setDateFrom($start_value) 83 + ->setDateTo($end_value) 84 + ->setDescription($description) 85 + ->save(); 86 + } catch (PhabricatorUserStatusInvalidEpochException $e) { 87 + $errors[] = 'Start must be before end.'; 88 + } catch (PhabricatorUserStatusOverlapException $e) { 89 + $errors[] = 'There is already a status within the specified '. 90 + 'timeframe. Edit or delete this existing status.'; 91 + } 92 + 93 + if (!$errors) { 94 + $uri = new PhutilURI($this->getApplicationURI()); 95 + $uri->setQueryParams( 96 + array( 97 + 'month' => phabricator_format_local_time($status->getDateFrom(), 98 + $user, 99 + 'm'), 100 + 'year' => phabricator_format_local_time($status->getDateFrom(), 101 + $user, 102 + 'Y'), 103 + $redirect => true, 104 + ) 105 + ); 106 + return id(new AphrontRedirectResponse()) 107 + ->setURI($uri); 108 + } 109 + } 110 + 111 + $error_view = null; 112 + if ($errors) { 113 + $error_view = id(new AphrontErrorView()) 114 + ->setTitle('Status can not be set!') 115 + ->setErrors($errors); 116 + } 117 + 118 + $status_select = id(new AphrontFormSelectControl()) 119 + ->setLabel(pht('Status')) 120 + ->setName('status') 121 + ->setOptions($status->getStatusOptions()); 122 + 123 + $description = id(new AphrontFormTextAreaControl()) 124 + ->setLabel(pht('Description')) 125 + ->setName('description') 126 + ->setValue($status->getDescription()); 127 + 128 + $form = id(new AphrontFormView()) 129 + ->setUser($user) 130 + ->setFlexible(true) 131 + ->appendChild($status_select) 132 + ->appendChild($start_time) 133 + ->appendChild($end_time) 134 + ->appendChild($description); 135 + 136 + $submit = id(new AphrontFormSubmitControl()) 137 + ->setValue($submit_label); 138 + if ($this->isCreate()) { 139 + $submit->addCancelButton($this->getApplicationURI()); 140 + } else { 141 + $submit->addCancelButton( 142 + $this->getApplicationURI('status/delete/'.$status->getID().'/'), 143 + 'Delete Status' 144 + ); 145 + } 146 + $form->appendChild($submit); 147 + 148 + $nav = $this->buildSideNavView($status); 149 + $nav->selectFilter($filter); 150 + 151 + $nav->appendChild( 152 + array( 153 + id(new PhabricatorHeaderView())->setHeader($page_title), 154 + $error_view, 155 + $form, 156 + ) 157 + ); 158 + 159 + return $this->buildApplicationPage( 160 + $nav, 161 + array( 162 + 'title' => $page_title, 163 + 'device' => true 164 + ) 165 + ); 166 + } 167 + 168 + }
+141
src/applications/calendar/controller/PhabricatorCalendarViewStatusController.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorCalendarViewStatusController 20 + extends PhabricatorCalendarController { 21 + 22 + private $phid; 23 + 24 + public function willProcessRequest(array $data) { 25 + $user = $this->getRequest()->getUser(); 26 + $this->phid = idx($data, 'phid', $user->getPHID()); 27 + $this->loadHandles(array($this->phid)); 28 + } 29 + 30 + public function processRequest() { 31 + 32 + $request = $this->getRequest(); 33 + $user = $request->getUser(); 34 + $handle = $this->getHandle($this->phid); 35 + $statuses = id(new PhabricatorUserStatus()) 36 + ->loadAllWhere('userPHID = %s AND dateTo > UNIX_TIMESTAMP()', 37 + $this->phid); 38 + 39 + $nav = $this->buildSideNavView(); 40 + $nav->selectFilter($this->getFilter()); 41 + 42 + $page_title = $this->getPageTitle(); 43 + 44 + $status_list = $this->buildStatusList($statuses); 45 + $status_list->setNoDataString($this->getNoDataString()); 46 + 47 + $nav->appendChild( 48 + array( 49 + id(new PhabricatorHeaderView())->setHeader($page_title), 50 + $status_list, 51 + ) 52 + ); 53 + 54 + return $this->buildApplicationPage( 55 + $nav, 56 + array( 57 + 'title' => $page_title, 58 + 'device' => true 59 + ) 60 + ); 61 + } 62 + 63 + private function buildStatusList(array $statuses) { 64 + assert_instances_of($statuses, 'PhabricatorUserStatus'); 65 + 66 + $user = $this->getRequest()->getUser(); 67 + 68 + $list = new PhabricatorObjectItemListView(); 69 + foreach ($statuses as $status) { 70 + if ($status->getUserPHID() == $user->getPHID()) { 71 + $href = $this->getApplicationURI('/status/edit/'.$status->getID().'/'); 72 + } else { 73 + $from = $status->getDateFrom(); 74 + $month = phabricator_format_local_time($from, $user, 'm'); 75 + $year = phabricator_format_local_time($from, $user, 'Y'); 76 + $uri = new PhutilURI($this->getApplicationURI()); 77 + $uri->setQueryParams( 78 + array( 79 + 'month' => $month, 80 + 'year' => $year, 81 + ) 82 + ); 83 + $href = (string) $uri; 84 + } 85 + $from = phabricator_datetime($status->getDateFrom(), $user); 86 + $to = phabricator_datetime($status->getDateTo(), $user); 87 + $item = id(new PhabricatorObjectItemView()) 88 + ->setHeader($status->getTerseSummary($user)) 89 + ->setHref($href) 90 + ->addDetail( 91 + pht('Description'), 92 + $status->getDescription()) 93 + ->addAttribute(pht('From %s', $from)) 94 + ->addAttribute(pht('To %s', $to)); 95 + 96 + $list->addItem($item); 97 + } 98 + 99 + return $list; 100 + } 101 + 102 + private function getNoDataString() { 103 + if ($this->isUserRequest()) { 104 + $no_data = 105 + pht('You do not have any upcoming status events.'); 106 + } else { 107 + $no_data = 108 + pht('%s does not have any upcoming status events.', 109 + phutil_escape_html($this->getHandle($this->phid)->getName())); 110 + } 111 + return $no_data; 112 + } 113 + 114 + private function getFilter() { 115 + if ($this->isUserRequest()) { 116 + $filter = 'status/'; 117 + } else { 118 + $filter = 'status/view/'.$this->phid.'/'; 119 + } 120 + 121 + return $filter; 122 + } 123 + 124 + private function getPageTitle() { 125 + if ($this->isUserRequest()) { 126 + $page_title = pht('Upcoming Statuses'); 127 + } else { 128 + $page_title = pht( 129 + 'Upcoming Statuses for %s', 130 + phutil_escape_html($this->getHandle($this->phid)->getName()) 131 + ); 132 + } 133 + return $page_title; 134 + } 135 + 136 + private function isUserRequest() { 137 + $user = $this->getRequest()->getUser(); 138 + return $this->phid == $user->getPHID(); 139 + } 140 + 141 + }
+79 -11
src/applications/calendar/view/AphrontCalendarMonthView.php
··· 23 23 private $year; 24 24 private $holidays = array(); 25 25 private $events = array(); 26 + private $browseURI; 27 + 28 + public function setBrowseURI($browse_uri) { 29 + $this->browseURI = $browse_uri; 30 + return $this; 31 + } 32 + private function getBrowseURI() { 33 + return $this->browseURI; 34 + } 26 35 27 36 public function setUser(PhabricatorUser $user) { 28 37 $this->user = $user; ··· 144 153 } 145 154 $table = 146 155 '<table class="aphront-calendar-view">'. 147 - '<tr class="aphront-calendar-month-year-header">'. 148 - '<th colspan="7">'.$first->format('F Y').'</th>'. 149 - '</tr>'. 150 - '<tr class="aphront-calendar-day-of-week-header">'. 156 + $this->renderCalendarHeader($first). 157 + '<tr class="aphront-calendar-day-of-week-header">'. 151 158 '<th>Sun</th>'. 152 159 '<th>Mon</th>'. 153 160 '<th>Tue</th>'. ··· 162 169 return $table; 163 170 } 164 171 172 + private function renderCalendarHeader(DateTime $date) { 173 + $colspan = 7; 174 + $left_th = ''; 175 + $right_th = ''; 176 + 177 + // check for a browseURI, which means we need "fancy" prev / next UI 178 + $uri = $this->getBrowseURI(); 179 + if ($uri) { 180 + $colspan = 5; 181 + $uri = new PhutilURI($uri); 182 + list($prev_year, $prev_month) = $this->getPrevYearAndMonth(); 183 + $query = array('year' => $prev_year, 'month' => $prev_month); 184 + $prev_link = phutil_render_tag( 185 + 'a', 186 + array('href' => (string) $uri->setQueryParams($query)), 187 + '&larr;' 188 + ); 189 + 190 + list($next_year, $next_month) = $this->getNextYearAndMonth(); 191 + $query = array('year' => $next_year, 'month' => $next_month); 192 + $next_link = phutil_render_tag( 193 + 'a', 194 + array('href' => (string) $uri->setQueryParams($query)), 195 + '&rarr;' 196 + ); 197 + 198 + $left_th = '<th>'.$prev_link.'</th>'; 199 + $right_th = '<th>'.$next_link.'</th>'; 200 + } 201 + 202 + return 203 + '<tr class="aphront-calendar-month-year-header">'. 204 + $left_th. 205 + '<th colspan="'.$colspan.'">'.$date->format('F Y').'</th>'. 206 + $right_th. 207 + '</tr>'; 208 + } 209 + 210 + private function getNextYearAndMonth() { 211 + $month = $this->month; 212 + $year = $this->year; 213 + 214 + $next_year = $year; 215 + $next_month = $month + 1; 216 + if ($next_month == 13) { 217 + $next_year = $year + 1; 218 + $next_month = 1; 219 + } 220 + 221 + return array($next_year, $next_month); 222 + } 223 + 224 + private function getPrevYearAndMonth() { 225 + $month = $this->month; 226 + $year = $this->year; 227 + 228 + $prev_year = $year; 229 + $prev_month = $month - 1; 230 + if ($prev_month == 0) { 231 + $prev_year = $year - 1; 232 + $prev_month = 12; 233 + } 234 + 235 + return array($prev_year, $prev_month); 236 + } 237 + 165 238 /** 166 239 * Return a DateTime object representing the first moment in each day in the 167 240 * month, according to the user's locale. ··· 176 249 $month = $this->month; 177 250 $year = $this->year; 178 251 179 - // Find the year and month numbers of the following month, so we can 252 + // Get the year and month numbers of the following month, so we can 180 253 // determine when this month ends. 181 - $next_year = $year; 182 - $next_month = $month + 1; 183 - if ($next_month == 13) { 184 - $next_year = $year + 1; 185 - $next_month = 1; 186 - } 254 + list($next_year, $next_month) = $this->getNextYearAndMonth(); 187 255 188 256 $end_date = new DateTime("{$next_year}-{$next_month}-01", $timezone); 189 257 $end_epoch = $end_date->format('U');
+20 -32
src/applications/conduit/method/user/ConduitAPI_user_addstatus_Method.php
··· 31 31 32 32 public function defineParamTypes() { 33 33 return array( 34 - 'fromEpoch' => 'required int', 35 - 'toEpoch' => 'required int', 36 - 'status' => 'required enum<away, sporadic>', 34 + 'fromEpoch' => 'required int', 35 + 'toEpoch' => 'required int', 36 + 'status' => 'required enum<away, sporadic>', 37 + 'description' => 'optional string', 37 38 ); 38 39 } 39 40 ··· 44 45 public function defineErrorTypes() { 45 46 return array( 46 47 'ERR-BAD-EPOCH' => "'toEpoch' must be bigger than 'fromEpoch'.", 47 - 'ERR-OVERLAP' => 48 + 'ERR-OVERLAP' => 48 49 'There must be no status in any part of the specified epoch.', 49 50 ); 50 51 } 51 52 52 53 protected function execute(ConduitAPIRequest $request) { 53 - $user_phid = $request->getUser()->getPHID(); 54 - $from = $request->getValue('fromEpoch'); 55 - $to = $request->getValue('toEpoch'); 54 + $user_phid = $request->getUser()->getPHID(); 55 + $from = $request->getValue('fromEpoch'); 56 + $to = $request->getValue('toEpoch'); 57 + $status = ucfirst($request->getValue('status')); 58 + $description = $request->getValue('description'); 56 59 57 - if ($to <= $from) { 60 + try { 61 + id(new PhabricatorUserStatus()) 62 + ->setUserPHID($user_phid) 63 + ->setDateFrom($from) 64 + ->setDateTo($to) 65 + ->setTextStatus($status) 66 + ->setDescription($description) 67 + ->save(); 68 + } catch (PhabricatorUserStatusInvalidEpochException $e) { 58 69 throw new ConduitException('ERR-BAD-EPOCH'); 59 - } 60 - 61 - $table = new PhabricatorUserStatus(); 62 - $table->openTransaction(); 63 - $table->beginWriteLocking(); 64 - 65 - $overlap = $table->loadAllWhere( 66 - 'userPHID = %s AND dateFrom < %d AND dateTo > %d', 67 - $user_phid, 68 - $to, 69 - $from); 70 - if ($overlap) { 71 - $table->endWriteLocking(); 72 - $table->killTransaction(); 70 + } catch (PhabricatorUserStatusOverlapException $e) { 73 71 throw new ConduitException('ERR-OVERLAP'); 74 72 } 75 - 76 - id(new PhabricatorUserStatus()) 77 - ->setUserPHID($user_phid) 78 - ->setDateFrom($from) 79 - ->setDateTo($to) 80 - ->setTextStatus($request->getValue('status')) 81 - ->save(); 82 - 83 - $table->endWriteLocking(); 84 - $table->saveTransaction(); 85 73 } 86 74 87 75 }
+1
src/applications/conduit/method/user/ConduitAPI_user_removestatus_Method.php
··· 73 73 ->setDateFrom($to) 74 74 ->setDateTo($status->getDateTo()) 75 75 ->setStatus($status->getStatus()) 76 + ->setDescription($status->getDescription()) 76 77 ->save(); 77 78 } 78 79 $status->setDateTo($from);
+1 -1
src/applications/people/controller/PhabricatorPeopleProfileController.php
··· 137 137 $statuses = id(new PhabricatorUserStatus())->loadCurrentStatuses( 138 138 array($user->getPHID())); 139 139 if ($statuses) { 140 - $header->setStatus(reset($statuses)->getStatusDescription($viewer)); 140 + $header->setStatus(reset($statuses)->getTerseSummary($viewer)); 141 141 } 142 142 } 143 143
+20
src/applications/people/exception/PhabricatorUserStatusInvalidEpochException.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorUserStatusInvalidEpochException extends Exception { 20 + }
+20
src/applications/people/exception/PhabricatorUserStatusOverlapException.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorUserStatusOverlapException extends Exception { 20 + }
+49 -11
src/applications/people/storage/PhabricatorUserStatus.php
··· 18 18 19 19 final class PhabricatorUserStatus extends PhabricatorUserDAO { 20 20 21 - const STATUS_AWAY = 1; 22 - const STATUS_SPORADIC = 2; 23 - 24 - private static $statusTexts = array( 25 - self::STATUS_AWAY => 'away', 26 - self::STATUS_SPORADIC => 'sporadic', 27 - ); 28 - 29 21 protected $userPHID; 30 22 protected $dateFrom; 31 23 protected $dateTo; 32 24 protected $status; 25 + protected $description; 26 + 27 + const STATUS_AWAY = 1; 28 + const STATUS_SPORADIC = 2; 29 + 30 + public function getStatusOptions() { 31 + return array( 32 + self::STATUS_AWAY => pht('Away'), 33 + self::STATUS_SPORADIC => pht('Sporadic'), 34 + ); 35 + } 33 36 34 37 public function getTextStatus() { 35 - return self::$statusTexts[$this->status]; 38 + $options = $this->getStatusOptions(); 39 + return $options[$this->status]; 36 40 } 37 41 38 - public function getStatusDescription(PhabricatorUser $viewer) { 42 + public function getTerseSummary(PhabricatorUser $viewer) { 39 43 $until = phabricator_date($this->dateTo, $viewer); 40 44 if ($this->status == PhabricatorUserStatus::STATUS_SPORADIC) { 41 45 return 'Sporadic until '.$until; ··· 45 49 } 46 50 47 51 public function setTextStatus($status) { 48 - $statuses = array_flip(self::$statusTexts); 52 + $statuses = array_flip($this->getStatusOptions()); 49 53 return $this->setStatus($statuses[$status]); 50 54 } 51 55 ··· 54 58 'userPHID IN (%Ls) AND UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo', 55 59 $user_phids); 56 60 return mpull($statuses, null, 'getUserPHID'); 61 + } 62 + 63 + /** 64 + * Validates data and throws exceptions for non-sensical status 65 + * windows and attempts to create an overlapping status. 66 + */ 67 + public function save() { 68 + 69 + if ($this->getDateTo() <= $this->getDateFrom()) { 70 + throw new PhabricatorUserStatusInvalidEpochException(); 71 + } 72 + 73 + $this->openTransaction(); 74 + $this->beginWriteLocking(); 75 + 76 + if ($this->shouldInsertWhenSaved()) { 77 + 78 + $overlap = $this->loadAllWhere( 79 + 'userPHID = %s AND dateFrom < %d AND dateTo > %d', 80 + $this->getUserPHID(), 81 + $this->getDateTo(), 82 + $this->getDateFrom()); 83 + 84 + if ($overlap) { 85 + $this->endWriteLocking(); 86 + $this->killTransaction(); 87 + throw new PhabricatorUserStatusOverlapException(); 88 + } 89 + } 90 + 91 + parent::save(); 92 + 93 + $this->endWriteLocking(); 94 + return $this->saveTransaction(); 57 95 } 58 96 59 97 }
+4
src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
··· 1012 1012 'type' => 'sql', 1013 1013 'name' => $this->getPatchPath('phameoneblog.sql'), 1014 1014 ), 1015 + 'statustxt.sql' => array( 1016 + 'type' => 'sql', 1017 + 'name' => $this->getPatchPath('statustxt.sql'), 1018 + ), 1015 1019 ); 1016 1020 } 1017 1021
+6
webroot/rsrc/css/aphront/calendar-view.css
··· 18 18 background: #003366; 19 19 } 20 20 21 + tr.aphront-calendar-month-year-header th a { 22 + color: white; 23 + font-weight: bold; 24 + text-decoration: none; 25 + } 26 + 21 27 tr.aphront-calendar-day-of-week-header th { 22 28 text-align: center; 23 29 font-size: 11px;