diff --git a/resources/sql/patches/statustxt.sql b/resources/sql/patches/statustxt.sql new file mode 100644 index 0000000000..8cd1c033f9 --- /dev/null +++ b/resources/sql/patches/statustxt.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_user.user_status + ADD description LONGTEXT NOT NULL COLLATE utf8_bin; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index efe0c3d867..7691b260b7 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -553,6 +553,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationApplications' => 'applications/meta/application/PhabricatorApplicationApplications.php', 'PhabricatorApplicationAudit' => 'applications/audit/application/PhabricatorApplicationAudit.php', 'PhabricatorApplicationAuth' => 'applications/auth/application/PhabricatorApplicationAuth.php', + 'PhabricatorApplicationCalendar' => 'applications/calendar/application/PhabricatorApplicationCalendar.php', 'PhabricatorApplicationConduit' => 'applications/conduit/application/PhabricatorApplicationConduit.php', 'PhabricatorApplicationCountdown' => 'applications/countdown/application/PhabricatorApplicationCountdown.php', 'PhabricatorApplicationDaemons' => 'applications/daemon/application/PhabricatorApplicationDaemons.php', @@ -609,8 +610,11 @@ phutil_register_library_map(array( 'PhabricatorCalendarBrowseController' => 'applications/calendar/controller/PhabricatorCalendarBrowseController.php', 'PhabricatorCalendarController' => 'applications/calendar/controller/PhabricatorCalendarController.php', 'PhabricatorCalendarDAO' => 'applications/calendar/storage/PhabricatorCalendarDAO.php', + 'PhabricatorCalendarDeleteStatusController' => 'applications/calendar/controller/PhabricatorCalendarDeleteStatusController.php', + 'PhabricatorCalendarEditStatusController' => 'applications/calendar/controller/PhabricatorCalendarEditStatusController.php', 'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php', 'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php', + 'PhabricatorCalendarViewStatusController' => 'applications/calendar/controller/PhabricatorCalendarViewStatusController.php', 'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php', 'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php', 'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/PhabricatorChatLogChannelLogController.php', @@ -1125,6 +1129,8 @@ phutil_register_library_map(array( 'PhabricatorUserProfile' => 'applications/people/storage/PhabricatorUserProfile.php', 'PhabricatorUserSSHKey' => 'applications/settings/storage/PhabricatorUserSSHKey.php', 'PhabricatorUserStatus' => 'applications/people/storage/PhabricatorUserStatus.php', + 'PhabricatorUserStatusInvalidEpochException' => 'applications/people/exception/PhabricatorUserStatusInvalidEpochException.php', + 'PhabricatorUserStatusOverlapException' => 'applications/people/exception/PhabricatorUserStatusOverlapException.php', 'PhabricatorUserTestCase' => 'applications/people/storage/__tests__/PhabricatorUserTestCase.php', 'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php', 'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php', @@ -1748,6 +1754,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationApplications' => 'PhabricatorApplication', 'PhabricatorApplicationAudit' => 'PhabricatorApplication', 'PhabricatorApplicationAuth' => 'PhabricatorApplication', + 'PhabricatorApplicationCalendar' => 'PhabricatorApplication', 'PhabricatorApplicationConduit' => 'PhabricatorApplication', 'PhabricatorApplicationCountdown' => 'PhabricatorApplication', 'PhabricatorApplicationDaemons' => 'PhabricatorApplication', @@ -1780,7 +1787,11 @@ phutil_register_library_map(array( 'PhabricatorApplicationUIExamples' => 'PhabricatorApplication', 'PhabricatorApplicationsListController' => 'PhabricatorController', 'PhabricatorAuditAddCommentController' => 'PhabricatorAuditController', - 'PhabricatorAuditComment' => 'PhabricatorAuditDAO', + 'PhabricatorAuditComment' => + array( + 0 => 'PhabricatorAuditDAO', + 1 => 'PhabricatorMarkupInterface', + ), 'PhabricatorAuditCommentEditor' => 'PhabricatorEditor', 'PhabricatorAuditCommitListView' => 'AphrontView', 'PhabricatorAuditController' => 'PhabricatorController', @@ -1803,8 +1814,11 @@ phutil_register_library_map(array( 'PhabricatorCalendarBrowseController' => 'PhabricatorCalendarController', 'PhabricatorCalendarController' => 'PhabricatorController', 'PhabricatorCalendarDAO' => 'PhabricatorLiskDAO', + 'PhabricatorCalendarDeleteStatusController' => 'PhabricatorCalendarController', + 'PhabricatorCalendarEditStatusController' => 'PhabricatorCalendarController', 'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO', 'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase', + 'PhabricatorCalendarViewStatusController' => 'PhabricatorCalendarController', 'PhabricatorChangesetResponse' => 'AphrontProxyResponse', 'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController', 'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController', @@ -2265,6 +2279,8 @@ phutil_register_library_map(array( 'PhabricatorUserProfile' => 'PhabricatorUserDAO', 'PhabricatorUserSSHKey' => 'PhabricatorUserDAO', 'PhabricatorUserStatus' => 'PhabricatorUserDAO', + 'PhabricatorUserStatusInvalidEpochException' => 'Exception', + 'PhabricatorUserStatusOverlapException' => 'Exception', 'PhabricatorUserTestCase' => 'PhabricatorTestCase', 'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO', 'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO', diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php index 33be0b29f9..7ba451b802 100644 --- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php @@ -113,10 +113,6 @@ class AphrontDefaultApplicationConfiguration 'keyboardshortcut/' => 'PhabricatorHelpKeyboardShortcutController', ), - '/calendar/' => array( - '' => 'PhabricatorCalendarBrowseController', - ), - '/drydock/' => array( '' => 'DrydockResourceListController', 'resource/' => 'DrydockResourceListController', diff --git a/src/applications/calendar/application/PhabricatorApplicationCalendar.php b/src/applications/calendar/application/PhabricatorApplicationCalendar.php new file mode 100644 index 0000000000..a819d84af6 --- /dev/null +++ b/src/applications/calendar/application/PhabricatorApplicationCalendar.php @@ -0,0 +1,58 @@ + array( + '' => 'PhabricatorCalendarBrowseController', + 'status/' => array( + '' => 'PhabricatorCalendarViewStatusController', + 'create/' => + 'PhabricatorCalendarEditStatusController', + 'delete/(?P[1-9]\d*)/' => + 'PhabricatorCalendarDeleteStatusController', + 'edit/(?P[1-9]\d*)/' => + 'PhabricatorCalendarEditStatusController', + 'view/(?P[^/]+)/' => + 'PhabricatorCalendarViewStatusController', + ), + ), + ); + } + +} diff --git a/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php b/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php index 9f3bcb3f95..e74f021bda 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php @@ -20,12 +20,13 @@ final class PhabricatorCalendarBrowseController extends PhabricatorCalendarController { public function processRequest() { + $now = time(); $request = $this->getRequest(); - $user = $request->getUser(); - - // TODO: These should be user-based and navigable in the interface. - $year = idate('Y'); - $month = idate('m'); + $user = $request->getUser(); + $year_d = phabricator_format_local_time($now, $user, 'Y'); + $year = $request->getInt('year', $year_d); + $month_d = phabricator_format_local_time($now, $user, 'm'); + $month = $request->getInt('month', $month_d); $holidays = id(new PhabricatorCalendarHoliday())->loadAllWhere( 'day BETWEEN %s AND %s', @@ -39,6 +40,7 @@ final class PhabricatorCalendarBrowseController strtotime("{$year}-{$month}-01 next month")); $month_view = new AphrontCalendarMonthView($month, $year); + $month_view->setBrowseURI($request->getRequestURI()); $month_view->setUser($user); $month_view->setHolidays($holidays); @@ -53,18 +55,53 @@ final class PhabricatorCalendarBrowseController $status_text = $status->getTextStatus(); $event->setUserPHID($status->getUserPHID()); $event->setName("{$name_text} ({$status_text})"); - $event->setDescription($status->getStatusDescription($user)); + $details = ''; + if ($status->getDescription()) { + $details = "\n\n".rtrim(phutil_escape_html($status->getDescription())); + } + $event->setDescription( + $status->getTerseSummary($user).$details + ); $month_view->addEvent($event); } - return $this->buildStandardPageResponse( + $nav = $this->buildSideNavView(); + $nav->selectFilter('edit'); + $nav->appendChild( array( + $this->getNoticeView(), '
', $month_view, '
', - ), - array( + )); + + return $this->buildApplicationPage( + $nav, + array( 'title' => 'Calendar', + 'device' => true, )); } + + private function getNoticeView() { + $request = $this->getRequest(); + $view = null; + + if ($request->getExists('created')) { + $view = id(new AphrontErrorView()) + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) + ->setTitle(pht('Successfully created your status.')); + } else if ($request->getExists('updated')) { + $view = id(new AphrontErrorView()) + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) + ->setTitle(pht('Successfully updated your status.')); + } else if ($request->getExists('deleted')) { + $view = id(new AphrontErrorView()) + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) + ->setTitle(pht('Successfully deleted your status.')); + } + + return $view; + } + } diff --git a/src/applications/calendar/controller/PhabricatorCalendarController.php b/src/applications/calendar/controller/PhabricatorCalendarController.php index b66f173562..d2ff7ec9bf 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarController.php @@ -18,21 +18,26 @@ abstract class PhabricatorCalendarController extends PhabricatorController { - public function buildStandardPageResponse($view, array $data) { - $page = $this->buildStandardPageView(); + protected function buildSideNavView(PhabricatorUserStatus $status = null) { + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - $page->setApplicationName('Calendar'); - $page->setBaseURI('/calendar/'); - $page->setTitle(idx($data, 'title')); + $nav->addFilter('', pht('Calendar'), $this->getApplicationURI()); - // Unicode has a calendar character but it's in some distant code plane, - // use "keyboard" since it looks vaguely similar. - $page->setGlyph("\xE2\x8C\xA8"); - $page->appendChild($view); + $nav->addSpacer(); - $response = new AphrontWebpageResponse(); - return $response->setContent($page->render()); + $nav->addLabel(pht('Create Events')); + $nav->addFilter('status/create/', pht('New Status')); + + $nav->addSpacer(); + $nav->addLabel(pht('Your Events')); + if ($status && $status->getID()) { + $nav->addFilter('status/edit/'.$status->getID().'/', pht('Edit Status')); + } + $nav->addFilter('status/', pht('Upcoming Statuses')); + + return $nav; } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarDeleteStatusController.php b/src/applications/calendar/controller/PhabricatorCalendarDeleteStatusController.php new file mode 100644 index 0000000000..673a2cd734 --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarDeleteStatusController.php @@ -0,0 +1,70 @@ +id = idx($data, 'id'); + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + $status = id(new PhabricatorUserStatus()) + ->loadOneWhere('id = %d', $this->id); + + if (!$status) { + return new Aphront404Response(); + } + if ($status->getUserPHID() != $user->getPHID()) { + return new Aphront403Response(); + } + + if ($request->isFormPost()) { + $status->delete(); + $uri = new PhutilURI($this->getApplicationURI()); + $uri->setQueryParams( + array( + 'deleted' => true, + ) + ); + return id(new AphrontRedirectResponse()) + ->setURI($uri); + } + + $dialog = new AphrontDialogView(); + $dialog->setUser($user); + $dialog->setTitle(pht('Really delete status?')); + $dialog->appendChild(phutil_render_tag( + 'p', + array(), + pht('Permanently delete this status? This action can not be undone.') + )); + $dialog->addSubmitButton(pht('Delete')); + $dialog->addCancelButton( + $this->getApplicationURI('status/edit/'.$status->getID().'/') + ); + + return id(new AphrontDialogResponse())->setDialog($dialog); + + } + +} diff --git a/src/applications/calendar/controller/PhabricatorCalendarEditStatusController.php b/src/applications/calendar/controller/PhabricatorCalendarEditStatusController.php new file mode 100644 index 0000000000..ec00701c05 --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarEditStatusController.php @@ -0,0 +1,168 @@ +id = idx($data, 'id'); + } + + public function isCreate() { + return !$this->id; + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $start_time = id(new AphrontFormDateControl()) + ->setUser($user) + ->setName('start') + ->setLabel(pht('Start')) + ->setInitialTime(AphrontFormDateControl::TIME_START_OF_DAY); + + $end_time = id(new AphrontFormDateControl()) + ->setUser($user) + ->setName('end') + ->setLabel(pht('End')) + ->setInitialTime(AphrontFormDateControl::TIME_END_OF_DAY); + + if ($this->isCreate()) { + $status = new PhabricatorUserStatus(); + $end_value = $end_time->readValueFromRequest($request); + $start_value = $start_time->readValueFromRequest($request); + $submit_label = pht('Create'); + $filter = 'status/create/'; + $page_title = pht('Create Status'); + $redirect = 'created'; + } else { + $status = id(new PhabricatorUserStatus()) + ->loadOneWhere('id = %d', $this->id); + $end_time->setValue($status->getDateTo()); + $start_time->setValue($status->getDateFrom()); + $submit_label = pht('Update'); + $filter = 'status/edit/'.$status->getID().'/'; + $page_title = pht('Update Status'); + $redirect = 'updated'; + + if ($status->getUserPHID() != $user->getPHID()) { + return new Aphront403Response(); + } + } + + $errors = array(); + if ($request->isFormPost()) { + $type = $request->getInt('status'); + $start_value = $start_time->readValueFromRequest($request); + $end_value = $end_time->readValueFromRequest($request); + $description = $request->getStr('description'); + + try { + $status + ->setUserPHID($user->getPHID()) + ->setStatus($type) + ->setDateFrom($start_value) + ->setDateTo($end_value) + ->setDescription($description) + ->save(); + } catch (PhabricatorUserStatusInvalidEpochException $e) { + $errors[] = 'Start must be before end.'; + } catch (PhabricatorUserStatusOverlapException $e) { + $errors[] = 'There is already a status within the specified '. + 'timeframe. Edit or delete this existing status.'; + } + + if (!$errors) { + $uri = new PhutilURI($this->getApplicationURI()); + $uri->setQueryParams( + array( + 'month' => phabricator_format_local_time($status->getDateFrom(), + $user, + 'm'), + 'year' => phabricator_format_local_time($status->getDateFrom(), + $user, + 'Y'), + $redirect => true, + ) + ); + return id(new AphrontRedirectResponse()) + ->setURI($uri); + } + } + + $error_view = null; + if ($errors) { + $error_view = id(new AphrontErrorView()) + ->setTitle('Status can not be set!') + ->setErrors($errors); + } + + $status_select = id(new AphrontFormSelectControl()) + ->setLabel(pht('Status')) + ->setName('status') + ->setOptions($status->getStatusOptions()); + + $description = id(new AphrontFormTextAreaControl()) + ->setLabel(pht('Description')) + ->setName('description') + ->setValue($status->getDescription()); + + $form = id(new AphrontFormView()) + ->setUser($user) + ->setFlexible(true) + ->appendChild($status_select) + ->appendChild($start_time) + ->appendChild($end_time) + ->appendChild($description); + + $submit = id(new AphrontFormSubmitControl()) + ->setValue($submit_label); + if ($this->isCreate()) { + $submit->addCancelButton($this->getApplicationURI()); + } else { + $submit->addCancelButton( + $this->getApplicationURI('status/delete/'.$status->getID().'/'), + 'Delete Status' + ); + } + $form->appendChild($submit); + + $nav = $this->buildSideNavView($status); + $nav->selectFilter($filter); + + $nav->appendChild( + array( + id(new PhabricatorHeaderView())->setHeader($page_title), + $error_view, + $form, + ) + ); + + return $this->buildApplicationPage( + $nav, + array( + 'title' => $page_title, + 'device' => true + ) + ); + } + +} diff --git a/src/applications/calendar/controller/PhabricatorCalendarViewStatusController.php b/src/applications/calendar/controller/PhabricatorCalendarViewStatusController.php new file mode 100644 index 0000000000..099147bcff --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarViewStatusController.php @@ -0,0 +1,141 @@ +getRequest()->getUser(); + $this->phid = idx($data, 'phid', $user->getPHID()); + $this->loadHandles(array($this->phid)); + } + + public function processRequest() { + + $request = $this->getRequest(); + $user = $request->getUser(); + $handle = $this->getHandle($this->phid); + $statuses = id(new PhabricatorUserStatus()) + ->loadAllWhere('userPHID = %s AND dateTo > UNIX_TIMESTAMP()', + $this->phid); + + $nav = $this->buildSideNavView(); + $nav->selectFilter($this->getFilter()); + + $page_title = $this->getPageTitle(); + + $status_list = $this->buildStatusList($statuses); + $status_list->setNoDataString($this->getNoDataString()); + + $nav->appendChild( + array( + id(new PhabricatorHeaderView())->setHeader($page_title), + $status_list, + ) + ); + + return $this->buildApplicationPage( + $nav, + array( + 'title' => $page_title, + 'device' => true + ) + ); + } + + private function buildStatusList(array $statuses) { + assert_instances_of($statuses, 'PhabricatorUserStatus'); + + $user = $this->getRequest()->getUser(); + + $list = new PhabricatorObjectItemListView(); + foreach ($statuses as $status) { + if ($status->getUserPHID() == $user->getPHID()) { + $href = $this->getApplicationURI('/status/edit/'.$status->getID().'/'); + } else { + $from = $status->getDateFrom(); + $month = phabricator_format_local_time($from, $user, 'm'); + $year = phabricator_format_local_time($from, $user, 'Y'); + $uri = new PhutilURI($this->getApplicationURI()); + $uri->setQueryParams( + array( + 'month' => $month, + 'year' => $year, + ) + ); + $href = (string) $uri; + } + $from = phabricator_datetime($status->getDateFrom(), $user); + $to = phabricator_datetime($status->getDateTo(), $user); + $item = id(new PhabricatorObjectItemView()) + ->setHeader($status->getTerseSummary($user)) + ->setHref($href) + ->addDetail( + pht('Description'), + $status->getDescription()) + ->addAttribute(pht('From %s', $from)) + ->addAttribute(pht('To %s', $to)); + + $list->addItem($item); + } + + return $list; + } + + private function getNoDataString() { + if ($this->isUserRequest()) { + $no_data = + pht('You do not have any upcoming status events.'); + } else { + $no_data = + pht('%s does not have any upcoming status events.', + phutil_escape_html($this->getHandle($this->phid)->getName())); + } + return $no_data; + } + + private function getFilter() { + if ($this->isUserRequest()) { + $filter = 'status/'; + } else { + $filter = 'status/view/'.$this->phid.'/'; + } + + return $filter; + } + + private function getPageTitle() { + if ($this->isUserRequest()) { + $page_title = pht('Upcoming Statuses'); + } else { + $page_title = pht( + 'Upcoming Statuses for %s', + phutil_escape_html($this->getHandle($this->phid)->getName()) + ); + } + return $page_title; + } + + private function isUserRequest() { + $user = $this->getRequest()->getUser(); + return $this->phid == $user->getPHID(); + } + +} diff --git a/src/applications/calendar/view/AphrontCalendarMonthView.php b/src/applications/calendar/view/AphrontCalendarMonthView.php index b95eff44c0..65c310dd14 100644 --- a/src/applications/calendar/view/AphrontCalendarMonthView.php +++ b/src/applications/calendar/view/AphrontCalendarMonthView.php @@ -23,6 +23,15 @@ final class AphrontCalendarMonthView extends AphrontView { private $year; private $holidays = array(); private $events = array(); + private $browseURI; + + public function setBrowseURI($browse_uri) { + $this->browseURI = $browse_uri; + return $this; + } + private function getBrowseURI() { + return $this->browseURI; + } public function setUser(PhabricatorUser $user) { $this->user = $user; @@ -144,10 +153,8 @@ final class AphrontCalendarMonthView extends AphrontView { } $table = ''. - ''. - ''. - ''. - ''. + $this->renderCalendarHeader($first). + ''. ''. ''. ''. @@ -162,6 +169,72 @@ final class AphrontCalendarMonthView extends AphrontView { return $table; } + private function renderCalendarHeader(DateTime $date) { + $colspan = 7; + $left_th = ''; + $right_th = ''; + + // check for a browseURI, which means we need "fancy" prev / next UI + $uri = $this->getBrowseURI(); + if ($uri) { + $colspan = 5; + $uri = new PhutilURI($uri); + list($prev_year, $prev_month) = $this->getPrevYearAndMonth(); + $query = array('year' => $prev_year, 'month' => $prev_month); + $prev_link = phutil_render_tag( + 'a', + array('href' => (string) $uri->setQueryParams($query)), + '←' + ); + + list($next_year, $next_month) = $this->getNextYearAndMonth(); + $query = array('year' => $next_year, 'month' => $next_month); + $next_link = phutil_render_tag( + 'a', + array('href' => (string) $uri->setQueryParams($query)), + '→' + ); + + $left_th = ''; + $right_th = ''; + } + + return + ''. + $left_th. + ''. + $right_th. + ''; + } + + private function getNextYearAndMonth() { + $month = $this->month; + $year = $this->year; + + $next_year = $year; + $next_month = $month + 1; + if ($next_month == 13) { + $next_year = $year + 1; + $next_month = 1; + } + + return array($next_year, $next_month); + } + + private function getPrevYearAndMonth() { + $month = $this->month; + $year = $this->year; + + $prev_year = $year; + $prev_month = $month - 1; + if ($prev_month == 0) { + $prev_year = $year - 1; + $prev_month = 12; + } + + return array($prev_year, $prev_month); + } + /** * Return a DateTime object representing the first moment in each day in the * month, according to the user's locale. @@ -176,14 +249,9 @@ final class AphrontCalendarMonthView extends AphrontView { $month = $this->month; $year = $this->year; - // Find the year and month numbers of the following month, so we can + // Get the year and month numbers of the following month, so we can // determine when this month ends. - $next_year = $year; - $next_month = $month + 1; - if ($next_month == 13) { - $next_year = $year + 1; - $next_month = 1; - } + list($next_year, $next_month) = $this->getNextYearAndMonth(); $end_date = new DateTime("{$next_year}-{$next_month}-01", $timezone); $end_epoch = $end_date->format('U'); diff --git a/src/applications/conduit/method/user/ConduitAPI_user_addstatus_Method.php b/src/applications/conduit/method/user/ConduitAPI_user_addstatus_Method.php index 2789a00bff..782f01a6b6 100644 --- a/src/applications/conduit/method/user/ConduitAPI_user_addstatus_Method.php +++ b/src/applications/conduit/method/user/ConduitAPI_user_addstatus_Method.php @@ -31,9 +31,10 @@ final class ConduitAPI_user_addstatus_Method extends ConduitAPI_user_Method { public function defineParamTypes() { return array( - 'fromEpoch' => 'required int', - 'toEpoch' => 'required int', - 'status' => 'required enum', + 'fromEpoch' => 'required int', + 'toEpoch' => 'required int', + 'status' => 'required enum', + 'description' => 'optional string', ); } @@ -44,44 +45,31 @@ final class ConduitAPI_user_addstatus_Method extends ConduitAPI_user_Method { public function defineErrorTypes() { return array( 'ERR-BAD-EPOCH' => "'toEpoch' must be bigger than 'fromEpoch'.", - 'ERR-OVERLAP' => + 'ERR-OVERLAP' => 'There must be no status in any part of the specified epoch.', ); } protected function execute(ConduitAPIRequest $request) { - $user_phid = $request->getUser()->getPHID(); - $from = $request->getValue('fromEpoch'); - $to = $request->getValue('toEpoch'); + $user_phid = $request->getUser()->getPHID(); + $from = $request->getValue('fromEpoch'); + $to = $request->getValue('toEpoch'); + $status = ucfirst($request->getValue('status')); + $description = $request->getValue('description'); - if ($to <= $from) { + try { + id(new PhabricatorUserStatus()) + ->setUserPHID($user_phid) + ->setDateFrom($from) + ->setDateTo($to) + ->setTextStatus($status) + ->setDescription($description) + ->save(); + } catch (PhabricatorUserStatusInvalidEpochException $e) { throw new ConduitException('ERR-BAD-EPOCH'); - } - - $table = new PhabricatorUserStatus(); - $table->openTransaction(); - $table->beginWriteLocking(); - - $overlap = $table->loadAllWhere( - 'userPHID = %s AND dateFrom < %d AND dateTo > %d', - $user_phid, - $to, - $from); - if ($overlap) { - $table->endWriteLocking(); - $table->killTransaction(); + } catch (PhabricatorUserStatusOverlapException $e) { throw new ConduitException('ERR-OVERLAP'); } - - id(new PhabricatorUserStatus()) - ->setUserPHID($user_phid) - ->setDateFrom($from) - ->setDateTo($to) - ->setTextStatus($request->getValue('status')) - ->save(); - - $table->endWriteLocking(); - $table->saveTransaction(); } } diff --git a/src/applications/conduit/method/user/ConduitAPI_user_removestatus_Method.php b/src/applications/conduit/method/user/ConduitAPI_user_removestatus_Method.php index 2ee72182e7..be58bdac78 100644 --- a/src/applications/conduit/method/user/ConduitAPI_user_removestatus_Method.php +++ b/src/applications/conduit/method/user/ConduitAPI_user_removestatus_Method.php @@ -73,6 +73,7 @@ final class ConduitAPI_user_removestatus_Method extends ConduitAPI_user_Method { ->setDateFrom($to) ->setDateTo($status->getDateTo()) ->setStatus($status->getStatus()) + ->setDescription($status->getDescription()) ->save(); } $status->setDateTo($from); diff --git a/src/applications/people/controller/PhabricatorPeopleProfileController.php b/src/applications/people/controller/PhabricatorPeopleProfileController.php index 05c73ba469..54b5a5d501 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileController.php @@ -137,7 +137,7 @@ final class PhabricatorPeopleProfileController $statuses = id(new PhabricatorUserStatus())->loadCurrentStatuses( array($user->getPHID())); if ($statuses) { - $header->setStatus(reset($statuses)->getStatusDescription($viewer)); + $header->setStatus(reset($statuses)->getTerseSummary($viewer)); } } diff --git a/src/applications/people/exception/PhabricatorUserStatusInvalidEpochException.php b/src/applications/people/exception/PhabricatorUserStatusInvalidEpochException.php new file mode 100644 index 0000000000..e235f68a00 --- /dev/null +++ b/src/applications/people/exception/PhabricatorUserStatusInvalidEpochException.php @@ -0,0 +1,20 @@ + 'away', - self::STATUS_SPORADIC => 'sporadic', - ); - protected $userPHID; protected $dateFrom; protected $dateTo; protected $status; + protected $description; - public function getTextStatus() { - return self::$statusTexts[$this->status]; + const STATUS_AWAY = 1; + const STATUS_SPORADIC = 2; + + public function getStatusOptions() { + return array( + self::STATUS_AWAY => pht('Away'), + self::STATUS_SPORADIC => pht('Sporadic'), + ); } - public function getStatusDescription(PhabricatorUser $viewer) { + public function getTextStatus() { + $options = $this->getStatusOptions(); + return $options[$this->status]; + } + + public function getTerseSummary(PhabricatorUser $viewer) { $until = phabricator_date($this->dateTo, $viewer); if ($this->status == PhabricatorUserStatus::STATUS_SPORADIC) { return 'Sporadic until '.$until; @@ -45,7 +49,7 @@ final class PhabricatorUserStatus extends PhabricatorUserDAO { } public function setTextStatus($status) { - $statuses = array_flip(self::$statusTexts); + $statuses = array_flip($this->getStatusOptions()); return $this->setStatus($statuses[$status]); } @@ -56,4 +60,38 @@ final class PhabricatorUserStatus extends PhabricatorUserDAO { return mpull($statuses, null, 'getUserPHID'); } + /** + * Validates data and throws exceptions for non-sensical status + * windows and attempts to create an overlapping status. + */ + public function save() { + + if ($this->getDateTo() <= $this->getDateFrom()) { + throw new PhabricatorUserStatusInvalidEpochException(); + } + + $this->openTransaction(); + $this->beginWriteLocking(); + + if ($this->shouldInsertWhenSaved()) { + + $overlap = $this->loadAllWhere( + 'userPHID = %s AND dateFrom < %d AND dateTo > %d', + $this->getUserPHID(), + $this->getDateTo(), + $this->getDateFrom()); + + if ($overlap) { + $this->endWriteLocking(); + $this->killTransaction(); + throw new PhabricatorUserStatusOverlapException(); + } + } + + parent::save(); + + $this->endWriteLocking(); + return $this->saveTransaction(); + } + } diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index a739e51ebe..0f40f7504f 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -1012,6 +1012,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'type' => 'sql', 'name' => $this->getPatchPath('phameoneblog.sql'), ), + 'statustxt.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('statustxt.sql'), + ), ); } diff --git a/webroot/rsrc/css/aphront/calendar-view.css b/webroot/rsrc/css/aphront/calendar-view.css index e1b1a5962d..89518673b7 100644 --- a/webroot/rsrc/css/aphront/calendar-view.css +++ b/webroot/rsrc/css/aphront/calendar-view.css @@ -18,6 +18,12 @@ tr.aphront-calendar-month-year-header th { background: #003366; } +tr.aphront-calendar-month-year-header th a { + color: white; + font-weight: bold; + text-decoration: none; +} + tr.aphront-calendar-day-of-week-header th { text-align: center; font-size: 11px;
'.$first->format('F Y').'
SunMonTue'.$prev_link.''.$next_link.'
'.$date->format('F Y').'