1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

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
This commit is contained in:
Bob Trahan 2012-10-24 13:22:24 -07:00
parent 244b1302a0
commit 60466d3bcc
18 changed files with 719 additions and 81 deletions

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_user.user_status
ADD description LONGTEXT NOT NULL COLLATE utf8_bin;

View file

@ -553,6 +553,7 @@ phutil_register_library_map(array(
'PhabricatorApplicationApplications' => 'applications/meta/application/PhabricatorApplicationApplications.php', 'PhabricatorApplicationApplications' => 'applications/meta/application/PhabricatorApplicationApplications.php',
'PhabricatorApplicationAudit' => 'applications/audit/application/PhabricatorApplicationAudit.php', 'PhabricatorApplicationAudit' => 'applications/audit/application/PhabricatorApplicationAudit.php',
'PhabricatorApplicationAuth' => 'applications/auth/application/PhabricatorApplicationAuth.php', 'PhabricatorApplicationAuth' => 'applications/auth/application/PhabricatorApplicationAuth.php',
'PhabricatorApplicationCalendar' => 'applications/calendar/application/PhabricatorApplicationCalendar.php',
'PhabricatorApplicationConduit' => 'applications/conduit/application/PhabricatorApplicationConduit.php', 'PhabricatorApplicationConduit' => 'applications/conduit/application/PhabricatorApplicationConduit.php',
'PhabricatorApplicationCountdown' => 'applications/countdown/application/PhabricatorApplicationCountdown.php', 'PhabricatorApplicationCountdown' => 'applications/countdown/application/PhabricatorApplicationCountdown.php',
'PhabricatorApplicationDaemons' => 'applications/daemon/application/PhabricatorApplicationDaemons.php', 'PhabricatorApplicationDaemons' => 'applications/daemon/application/PhabricatorApplicationDaemons.php',
@ -609,8 +610,11 @@ phutil_register_library_map(array(
'PhabricatorCalendarBrowseController' => 'applications/calendar/controller/PhabricatorCalendarBrowseController.php', 'PhabricatorCalendarBrowseController' => 'applications/calendar/controller/PhabricatorCalendarBrowseController.php',
'PhabricatorCalendarController' => 'applications/calendar/controller/PhabricatorCalendarController.php', 'PhabricatorCalendarController' => 'applications/calendar/controller/PhabricatorCalendarController.php',
'PhabricatorCalendarDAO' => 'applications/calendar/storage/PhabricatorCalendarDAO.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', 'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php',
'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php', 'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php',
'PhabricatorCalendarViewStatusController' => 'applications/calendar/controller/PhabricatorCalendarViewStatusController.php',
'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php', 'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php',
'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php', 'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php',
'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/PhabricatorChatLogChannelLogController.php', 'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/PhabricatorChatLogChannelLogController.php',
@ -1125,6 +1129,8 @@ phutil_register_library_map(array(
'PhabricatorUserProfile' => 'applications/people/storage/PhabricatorUserProfile.php', 'PhabricatorUserProfile' => 'applications/people/storage/PhabricatorUserProfile.php',
'PhabricatorUserSSHKey' => 'applications/settings/storage/PhabricatorUserSSHKey.php', 'PhabricatorUserSSHKey' => 'applications/settings/storage/PhabricatorUserSSHKey.php',
'PhabricatorUserStatus' => 'applications/people/storage/PhabricatorUserStatus.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', 'PhabricatorUserTestCase' => 'applications/people/storage/__tests__/PhabricatorUserTestCase.php',
'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php', 'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php',
'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php', 'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php',
@ -1748,6 +1754,7 @@ phutil_register_library_map(array(
'PhabricatorApplicationApplications' => 'PhabricatorApplication', 'PhabricatorApplicationApplications' => 'PhabricatorApplication',
'PhabricatorApplicationAudit' => 'PhabricatorApplication', 'PhabricatorApplicationAudit' => 'PhabricatorApplication',
'PhabricatorApplicationAuth' => 'PhabricatorApplication', 'PhabricatorApplicationAuth' => 'PhabricatorApplication',
'PhabricatorApplicationCalendar' => 'PhabricatorApplication',
'PhabricatorApplicationConduit' => 'PhabricatorApplication', 'PhabricatorApplicationConduit' => 'PhabricatorApplication',
'PhabricatorApplicationCountdown' => 'PhabricatorApplication', 'PhabricatorApplicationCountdown' => 'PhabricatorApplication',
'PhabricatorApplicationDaemons' => 'PhabricatorApplication', 'PhabricatorApplicationDaemons' => 'PhabricatorApplication',
@ -1780,7 +1787,11 @@ phutil_register_library_map(array(
'PhabricatorApplicationUIExamples' => 'PhabricatorApplication', 'PhabricatorApplicationUIExamples' => 'PhabricatorApplication',
'PhabricatorApplicationsListController' => 'PhabricatorController', 'PhabricatorApplicationsListController' => 'PhabricatorController',
'PhabricatorAuditAddCommentController' => 'PhabricatorAuditController', 'PhabricatorAuditAddCommentController' => 'PhabricatorAuditController',
'PhabricatorAuditComment' => 'PhabricatorAuditDAO', 'PhabricatorAuditComment' =>
array(
0 => 'PhabricatorAuditDAO',
1 => 'PhabricatorMarkupInterface',
),
'PhabricatorAuditCommentEditor' => 'PhabricatorEditor', 'PhabricatorAuditCommentEditor' => 'PhabricatorEditor',
'PhabricatorAuditCommitListView' => 'AphrontView', 'PhabricatorAuditCommitListView' => 'AphrontView',
'PhabricatorAuditController' => 'PhabricatorController', 'PhabricatorAuditController' => 'PhabricatorController',
@ -1803,8 +1814,11 @@ phutil_register_library_map(array(
'PhabricatorCalendarBrowseController' => 'PhabricatorCalendarController', 'PhabricatorCalendarBrowseController' => 'PhabricatorCalendarController',
'PhabricatorCalendarController' => 'PhabricatorController', 'PhabricatorCalendarController' => 'PhabricatorController',
'PhabricatorCalendarDAO' => 'PhabricatorLiskDAO', 'PhabricatorCalendarDAO' => 'PhabricatorLiskDAO',
'PhabricatorCalendarDeleteStatusController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEditStatusController' => 'PhabricatorCalendarController',
'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO', 'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO',
'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase', 'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase',
'PhabricatorCalendarViewStatusController' => 'PhabricatorCalendarController',
'PhabricatorChangesetResponse' => 'AphrontProxyResponse', 'PhabricatorChangesetResponse' => 'AphrontProxyResponse',
'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController', 'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController',
'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController', 'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController',
@ -2265,6 +2279,8 @@ phutil_register_library_map(array(
'PhabricatorUserProfile' => 'PhabricatorUserDAO', 'PhabricatorUserProfile' => 'PhabricatorUserDAO',
'PhabricatorUserSSHKey' => 'PhabricatorUserDAO', 'PhabricatorUserSSHKey' => 'PhabricatorUserDAO',
'PhabricatorUserStatus' => 'PhabricatorUserDAO', 'PhabricatorUserStatus' => 'PhabricatorUserDAO',
'PhabricatorUserStatusInvalidEpochException' => 'Exception',
'PhabricatorUserStatusOverlapException' => 'Exception',
'PhabricatorUserTestCase' => 'PhabricatorTestCase', 'PhabricatorUserTestCase' => 'PhabricatorTestCase',
'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO', 'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO',
'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO', 'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO',

View file

@ -113,10 +113,6 @@ class AphrontDefaultApplicationConfiguration
'keyboardshortcut/' => 'PhabricatorHelpKeyboardShortcutController', 'keyboardshortcut/' => 'PhabricatorHelpKeyboardShortcutController',
), ),
'/calendar/' => array(
'' => 'PhabricatorCalendarBrowseController',
),
'/drydock/' => array( '/drydock/' => array(
'' => 'DrydockResourceListController', '' => 'DrydockResourceListController',
'resource/' => 'DrydockResourceListController', 'resource/' => 'DrydockResourceListController',

View file

@ -0,0 +1,58 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorApplicationCalendar extends PhabricatorApplication {
public function getShortDescription() {
return pht('Dates and Stuff');
}
public function getFlavorText() {
return pht('Never miss an episode ever again.');
}
public function getBaseURI() {
return '/calendar/';
}
public function getTitleGlyph() {
// Unicode has a calendar character but it's in some distant code plane,
// use "keyboard" since it looks vaguely similar.
return "\xE2\x8C\xA8";
}
public function getRoutes() {
return array(
'/calendar/' => array(
'' => 'PhabricatorCalendarBrowseController',
'status/' => array(
'' => 'PhabricatorCalendarViewStatusController',
'create/' =>
'PhabricatorCalendarEditStatusController',
'delete/(?P<id>[1-9]\d*)/' =>
'PhabricatorCalendarDeleteStatusController',
'edit/(?P<id>[1-9]\d*)/' =>
'PhabricatorCalendarEditStatusController',
'view/(?P<phid>[^/]+)/' =>
'PhabricatorCalendarViewStatusController',
),
),
);
}
}

View file

@ -20,12 +20,13 @@ final class PhabricatorCalendarBrowseController
extends PhabricatorCalendarController { extends PhabricatorCalendarController {
public function processRequest() { public function processRequest() {
$now = time();
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $user = $request->getUser();
$year_d = phabricator_format_local_time($now, $user, 'Y');
// TODO: These should be user-based and navigable in the interface. $year = $request->getInt('year', $year_d);
$year = idate('Y'); $month_d = phabricator_format_local_time($now, $user, 'm');
$month = idate('m'); $month = $request->getInt('month', $month_d);
$holidays = id(new PhabricatorCalendarHoliday())->loadAllWhere( $holidays = id(new PhabricatorCalendarHoliday())->loadAllWhere(
'day BETWEEN %s AND %s', 'day BETWEEN %s AND %s',
@ -39,6 +40,7 @@ final class PhabricatorCalendarBrowseController
strtotime("{$year}-{$month}-01 next month")); strtotime("{$year}-{$month}-01 next month"));
$month_view = new AphrontCalendarMonthView($month, $year); $month_view = new AphrontCalendarMonthView($month, $year);
$month_view->setBrowseURI($request->getRequestURI());
$month_view->setUser($user); $month_view->setUser($user);
$month_view->setHolidays($holidays); $month_view->setHolidays($holidays);
@ -53,18 +55,53 @@ final class PhabricatorCalendarBrowseController
$status_text = $status->getTextStatus(); $status_text = $status->getTextStatus();
$event->setUserPHID($status->getUserPHID()); $event->setUserPHID($status->getUserPHID());
$event->setName("{$name_text} ({$status_text})"); $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); $month_view->addEvent($event);
} }
return $this->buildStandardPageResponse( $nav = $this->buildSideNavView();
$nav->selectFilter('edit');
$nav->appendChild(
array( array(
$this->getNoticeView(),
'<div style="padding: 2em;">', '<div style="padding: 2em;">',
$month_view, $month_view,
'</div>', '</div>',
), ));
array(
return $this->buildApplicationPage(
$nav,
array(
'title' => 'Calendar', '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;
}
} }

View file

@ -18,21 +18,26 @@
abstract class PhabricatorCalendarController extends PhabricatorController { 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'); $nav->addFilter('', pht('Calendar'), $this->getApplicationURI());
$page->setBaseURI('/calendar/');
$page->setTitle(idx($data, 'title'));
// Unicode has a calendar character but it's in some distant code plane, $nav->addSpacer();
// use "keyboard" since it looks vaguely similar.
$page->setGlyph("\xE2\x8C\xA8");
$page->appendChild($view);
$response = new AphrontWebpageResponse(); $nav->addLabel(pht('Create Events'));
return $response->setContent($page->render()); $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;
} }
} }

View file

@ -0,0 +1,70 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorCalendarDeleteStatusController
extends PhabricatorCalendarController {
private $id;
public function willProcessRequest(array $data) {
$this->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);
}
}

View file

@ -0,0 +1,168 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorCalendarEditStatusController
extends PhabricatorCalendarController {
private $id;
public function willProcessRequest(array $data) {
$this->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
)
);
}
}

View file

@ -0,0 +1,141 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorCalendarViewStatusController
extends PhabricatorCalendarController {
private $phid;
public function willProcessRequest(array $data) {
$user = $this->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();
}
}

View file

@ -23,6 +23,15 @@ final class AphrontCalendarMonthView extends AphrontView {
private $year; private $year;
private $holidays = array(); private $holidays = array();
private $events = 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) { public function setUser(PhabricatorUser $user) {
$this->user = $user; $this->user = $user;
@ -144,10 +153,8 @@ final class AphrontCalendarMonthView extends AphrontView {
} }
$table = $table =
'<table class="aphront-calendar-view">'. '<table class="aphront-calendar-view">'.
'<tr class="aphront-calendar-month-year-header">'. $this->renderCalendarHeader($first).
'<th colspan="7">'.$first->format('F Y').'</th>'. '<tr class="aphront-calendar-day-of-week-header">'.
'</tr>'.
'<tr class="aphront-calendar-day-of-week-header">'.
'<th>Sun</th>'. '<th>Sun</th>'.
'<th>Mon</th>'. '<th>Mon</th>'.
'<th>Tue</th>'. '<th>Tue</th>'.
@ -162,6 +169,72 @@ final class AphrontCalendarMonthView extends AphrontView {
return $table; 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)),
'&larr;'
);
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)),
'&rarr;'
);
$left_th = '<th>'.$prev_link.'</th>';
$right_th = '<th>'.$next_link.'</th>';
}
return
'<tr class="aphront-calendar-month-year-header">'.
$left_th.
'<th colspan="'.$colspan.'">'.$date->format('F Y').'</th>'.
$right_th.
'</tr>';
}
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 * Return a DateTime object representing the first moment in each day in the
* month, according to the user's locale. * month, according to the user's locale.
@ -176,14 +249,9 @@ final class AphrontCalendarMonthView extends AphrontView {
$month = $this->month; $month = $this->month;
$year = $this->year; $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. // determine when this month ends.
$next_year = $year; list($next_year, $next_month) = $this->getNextYearAndMonth();
$next_month = $month + 1;
if ($next_month == 13) {
$next_year = $year + 1;
$next_month = 1;
}
$end_date = new DateTime("{$next_year}-{$next_month}-01", $timezone); $end_date = new DateTime("{$next_year}-{$next_month}-01", $timezone);
$end_epoch = $end_date->format('U'); $end_epoch = $end_date->format('U');

View file

@ -31,9 +31,10 @@ final class ConduitAPI_user_addstatus_Method extends ConduitAPI_user_Method {
public function defineParamTypes() { public function defineParamTypes() {
return array( return array(
'fromEpoch' => 'required int', 'fromEpoch' => 'required int',
'toEpoch' => 'required int', 'toEpoch' => 'required int',
'status' => 'required enum<away, sporadic>', 'status' => 'required enum<away, sporadic>',
'description' => 'optional string',
); );
} }
@ -44,44 +45,31 @@ final class ConduitAPI_user_addstatus_Method extends ConduitAPI_user_Method {
public function defineErrorTypes() { public function defineErrorTypes() {
return array( return array(
'ERR-BAD-EPOCH' => "'toEpoch' must be bigger than 'fromEpoch'.", '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.', 'There must be no status in any part of the specified epoch.',
); );
} }
protected function execute(ConduitAPIRequest $request) { protected function execute(ConduitAPIRequest $request) {
$user_phid = $request->getUser()->getPHID(); $user_phid = $request->getUser()->getPHID();
$from = $request->getValue('fromEpoch'); $from = $request->getValue('fromEpoch');
$to = $request->getValue('toEpoch'); $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'); throw new ConduitException('ERR-BAD-EPOCH');
} } catch (PhabricatorUserStatusOverlapException $e) {
$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();
throw new ConduitException('ERR-OVERLAP'); throw new ConduitException('ERR-OVERLAP');
} }
id(new PhabricatorUserStatus())
->setUserPHID($user_phid)
->setDateFrom($from)
->setDateTo($to)
->setTextStatus($request->getValue('status'))
->save();
$table->endWriteLocking();
$table->saveTransaction();
} }
} }

View file

@ -73,6 +73,7 @@ final class ConduitAPI_user_removestatus_Method extends ConduitAPI_user_Method {
->setDateFrom($to) ->setDateFrom($to)
->setDateTo($status->getDateTo()) ->setDateTo($status->getDateTo())
->setStatus($status->getStatus()) ->setStatus($status->getStatus())
->setDescription($status->getDescription())
->save(); ->save();
} }
$status->setDateTo($from); $status->setDateTo($from);

View file

@ -137,7 +137,7 @@ final class PhabricatorPeopleProfileController
$statuses = id(new PhabricatorUserStatus())->loadCurrentStatuses( $statuses = id(new PhabricatorUserStatus())->loadCurrentStatuses(
array($user->getPHID())); array($user->getPHID()));
if ($statuses) { if ($statuses) {
$header->setStatus(reset($statuses)->getStatusDescription($viewer)); $header->setStatus(reset($statuses)->getTerseSummary($viewer));
} }
} }

View file

@ -0,0 +1,20 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorUserStatusInvalidEpochException extends Exception {
}

View file

@ -0,0 +1,20 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorUserStatusOverlapException extends Exception {
}

View file

@ -18,24 +18,28 @@
final class PhabricatorUserStatus extends PhabricatorUserDAO { final class PhabricatorUserStatus extends PhabricatorUserDAO {
const STATUS_AWAY = 1;
const STATUS_SPORADIC = 2;
private static $statusTexts = array(
self::STATUS_AWAY => 'away',
self::STATUS_SPORADIC => 'sporadic',
);
protected $userPHID; protected $userPHID;
protected $dateFrom; protected $dateFrom;
protected $dateTo; protected $dateTo;
protected $status; protected $status;
protected $description;
public function getTextStatus() { const STATUS_AWAY = 1;
return self::$statusTexts[$this->status]; 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); $until = phabricator_date($this->dateTo, $viewer);
if ($this->status == PhabricatorUserStatus::STATUS_SPORADIC) { if ($this->status == PhabricatorUserStatus::STATUS_SPORADIC) {
return 'Sporadic until '.$until; return 'Sporadic until '.$until;
@ -45,7 +49,7 @@ final class PhabricatorUserStatus extends PhabricatorUserDAO {
} }
public function setTextStatus($status) { public function setTextStatus($status) {
$statuses = array_flip(self::$statusTexts); $statuses = array_flip($this->getStatusOptions());
return $this->setStatus($statuses[$status]); return $this->setStatus($statuses[$status]);
} }
@ -56,4 +60,38 @@ final class PhabricatorUserStatus extends PhabricatorUserDAO {
return mpull($statuses, null, 'getUserPHID'); 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();
}
} }

View file

@ -1012,6 +1012,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'sql', 'type' => 'sql',
'name' => $this->getPatchPath('phameoneblog.sql'), 'name' => $this->getPatchPath('phameoneblog.sql'),
), ),
'statustxt.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('statustxt.sql'),
),
); );
} }

View file

@ -18,6 +18,12 @@ tr.aphront-calendar-month-year-header th {
background: #003366; 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 { tr.aphront-calendar-day-of-week-header th {
text-align: center; text-align: center;
font-size: 11px; font-size: 11px;