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:
parent
244b1302a0
commit
60466d3bcc
18 changed files with 719 additions and 81 deletions
2
resources/sql/patches/statustxt.sql
Normal file
2
resources/sql/patches/statustxt.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_user.user_status
|
||||
ADD description LONGTEXT NOT NULL COLLATE utf8_bin;
|
|
@ -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',
|
||||
|
|
|
@ -113,10 +113,6 @@ class AphrontDefaultApplicationConfiguration
|
|||
'keyboardshortcut/' => 'PhabricatorHelpKeyboardShortcutController',
|
||||
),
|
||||
|
||||
'/calendar/' => array(
|
||||
'' => 'PhabricatorCalendarBrowseController',
|
||||
),
|
||||
|
||||
'/drydock/' => array(
|
||||
'' => 'DrydockResourceListController',
|
||||
'resource/' => 'DrydockResourceListController',
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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(),
|
||||
'<div style="padding: 2em;">',
|
||||
$month_view,
|
||||
'</div>',
|
||||
),
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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 =
|
||||
'<table class="aphront-calendar-view">'.
|
||||
'<tr class="aphront-calendar-month-year-header">'.
|
||||
'<th colspan="7">'.$first->format('F Y').'</th>'.
|
||||
'</tr>'.
|
||||
'<tr class="aphront-calendar-day-of-week-header">'.
|
||||
$this->renderCalendarHeader($first).
|
||||
'<tr class="aphront-calendar-day-of-week-header">'.
|
||||
'<th>Sun</th>'.
|
||||
'<th>Mon</th>'.
|
||||
'<th>Tue</th>'.
|
||||
|
@ -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 = '<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
|
||||
* 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');
|
||||
|
|
|
@ -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<away, sporadic>',
|
||||
'fromEpoch' => 'required int',
|
||||
'toEpoch' => 'required int',
|
||||
'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() {
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -18,24 +18,28 @@
|
|||
|
||||
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 $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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue