diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php index 0b1be3860..01875351b 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php @@ -1,229 +1,279 @@ <?php final class PhabricatorCalendarEventEditController 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(); $user_phid = $user->getPHID(); $error_name = true; $validation_exception = null; - $invitees = null; $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()) { $event = PhabricatorCalendarEvent::initializeNewCalendarEvent($user); $end_value = $end_time->readValueFromRequest($request); $start_value = $start_time->readValueFromRequest($request); $submit_label = pht('Create'); $filter = 'event/create/'; $page_title = pht('Create Event'); $redirect = 'created'; $subscribers = array(); - $invitees = array( - $user_phid => PhabricatorCalendarEventInvitee::STATUS_ATTENDING, - ); + $invitees = array($user_phid); } else { $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$event) { return new Aphront404Response(); } $end_time->setValue($event->getDateTo()); $start_time->setValue($event->getDateFrom()); $submit_label = pht('Update'); $filter = 'event/edit/'.$event->getID().'/'; $page_title = pht('Update Event'); $redirect = 'updated'; $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $event->getPHID()); + $invitees = array(); + foreach ($event->getInvitees() as $invitee) { + if ($invitee->isUninvited()) { + continue; + } else { + $invitees[] = $invitee->getInviteePHID(); + } + } } $errors = array(); if ($request->isFormPost()) { $xactions = array(); $name = $request->getStr('name'); $type = $request->getInt('status'); $start_value = $start_time->readValueFromRequest($request); $end_value = $end_time->readValueFromRequest($request); $description = $request->getStr('description'); $subscribers = $request->getArr('subscribers'); + $invitees = $request->getArr('invitees'); + $new_invitees = $this->getNewInviteeList($invitees, $event); + + if ($this->isCreate()) { + $status = idx($new_invitees, $user->getPHID()); + $status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; + if ($status) { + $new_invitees[$user->getPHID()] = $status_attending; + } + } if ($start_time->getError()) { $errors[] = pht('Invalid start time; reset to default.'); } if ($end_time->getError()) { $errors[] = pht('Invalid end time; reset to default.'); } if (!$errors) { $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_NAME) ->setNewValue($name); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_START_DATE) ->setNewValue($start_value); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_END_DATE) ->setNewValue($end_value); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_STATUS) ->setNewValue($type); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue(array('=' => array_fuse($subscribers))); + $xactions[] = id(new PhabricatorCalendarEventTransaction()) + ->setTransactionType( + PhabricatorCalendarEventTransaction::TYPE_INVITE) + ->setNewValue($new_invitees); + $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION) ->setNewValue($description); - if ($invitees) { - $xactions[] = id(new PhabricatorCalendarEventTransaction()) - ->setTransactionType( - PhabricatorCalendarEventTransaction::TYPE_INVITE) - ->setNewValue($invitees); - } - - $editor = id(new PhabricatorCalendarEventEditor()) ->setActor($user) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $xactions = $editor->applyTransactions($event, $xactions); $response = id(new AphrontRedirectResponse()); return $response->setURI('/E'.$event->getID()); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $error_name = $ex ->getShortMessage(PhabricatorCalendarEventTransaction::TYPE_NAME); } } } $error_view = null; if ($errors) { $error_view = id(new PHUIInfoView()) ->setTitle(pht('Status can not be set!')) ->setErrors($errors); } $name = id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($event->getName()) ->setError($error_name); $status_select = id(new AphrontFormSelectControl()) ->setLabel(pht('Status')) ->setName('status') ->setValue($event->getStatus()) ->setOptions($event->getStatusOptions()); $description = id(new AphrontFormTextAreaControl()) ->setLabel(pht('Description')) ->setName('description') ->setValue($event->getDescription()); $subscribers = id(new AphrontFormTokenizerControl()) ->setLabel(pht('Subscribers')) ->setName('subscribers') ->setValue($subscribers) ->setUser($user) ->setDatasource(new PhabricatorMetaMTAMailableDatasource()); + $invitees = id(new AphrontFormTokenizerControl()) + ->setLabel(pht('Invitees')) + ->setName('invitees') + ->setValue($invitees) + ->setUser($user) + ->setDatasource(new PhabricatorMetaMTAMailableDatasource()); + $form = id(new AphrontFormView()) ->setUser($user) ->appendChild($name) ->appendChild($status_select) ->appendChild($start_time) ->appendChild($end_time) ->appendControl($subscribers) + ->appendControl($invitees) ->appendChild($description); $submit = id(new AphrontFormSubmitControl()) ->setValue($submit_label); if ($this->isCreate()) { $submit->addCancelButton($this->getApplicationURI()); } else { $submit->addCancelButton('/E'.$event->getID()); } $form->appendChild($submit); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setFormErrors($errors) ->setForm($form); $nav = $this->buildSideNavView($event); $nav->selectFilter($filter); $crumbs = $this->buildApplicationCrumbs(); if (!$this->isCreate()) { $crumbs->addTextCrumb('E'.$event->getId(), '/E'.$event->getId()); } $crumbs->addTextCrumb($page_title); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setValidationException($validation_exception) ->appendChild($form); $nav->appendChild( array( $crumbs, $object_box, )); return $this->buildApplicationPage( $nav, array( 'title' => $page_title, )); } + + public function getNewInviteeList(array $phids, $event) { + $invitees = $event->getInvitees(); + $invitees = mpull($invitees, null, 'getInviteePHID'); + $invited_status = PhabricatorCalendarEventInvitee::STATUS_INVITED; + $uninvited_status = PhabricatorCalendarEventInvitee::STATUS_UNINVITED; + $phids = array_fuse($phids); + + $new = array(); + foreach ($phids as $phid) { + $old_invitee = idx($invitees, $phid); + if ($old_invitee) { + $old_status = $old_invitee->getStatus(); + if ($old_status != $uninvited_status) { + continue; + } + } + $new[$phid] = $invited_status; + } + + foreach ($invitees as $invitee) { + $deleted_invitee = !idx($phids, $invitee->getInviteePHID()); + if ($deleted_invitee) { + $new[$invitee->getInviteePHID()] = $uninvited_status; + } + } + + return $new; + } + } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php index fa7ef9cc3..e0c82edb5 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php @@ -1,154 +1,158 @@ <?php final class PhabricatorCalendarEventViewController extends PhabricatorCalendarController { private $id; public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$event) { return new Aphront404Response(); } $title = 'E'.$event->getID(); $page_title = $title.' '.$event->getName(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title, '/E'.$event->getID()); $timeline = $this->buildTransactionTimeline( $event, new PhabricatorCalendarEventTransactionQuery()); $header = $this->buildHeaderView($event); $actions = $this->buildActionView($event); $properties = $this->buildPropertyView($event); $properties->setActionList($actions); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $box, $timeline, ), array( 'title' => $page_title, )); } private function buildHeaderView(PhabricatorCalendarEvent $event) { $viewer = $this->getRequest()->getUser(); $is_cancelled = $event->getIsCancelled(); $icon = $is_cancelled ? ('fa-times') : ('fa-calendar'); $color = $is_cancelled ? ('grey') : ('green'); $status = $is_cancelled ? ('Cancelled') : ('Active'); return id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($event->getName()) ->setStatus($icon, $color, $status) ->setPolicyObject($event); } private function buildActionView(PhabricatorCalendarEvent $event) { $viewer = $this->getRequest()->getUser(); $id = $event->getID(); $is_cancelled = $event->getIsCancelled(); $actions = id(new PhabricatorActionListView()) ->setObjectURI($this->getApplicationURI('event/'.$id.'/')) ->setUser($viewer) ->setObject($event); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $event, PhabricatorPolicyCapability::CAN_EDIT); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Event')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI("event/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($is_cancelled) { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Reinstate Event')) ->setIcon('fa-plus') ->setHref($this->getApplicationURI("event/cancel/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(true)); } else { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Cancel Event')) ->setIcon('fa-times') ->setHref($this->getApplicationURI("event/cancel/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(true)); } return $actions; } private function buildPropertyView(PhabricatorCalendarEvent $event) { $viewer = $this->getRequest()->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($event); $properties->addProperty( pht('Starts'), phabricator_datetime($event->getDateFrom(), $viewer)); $properties->addProperty( pht('Ends'), phabricator_datetime($event->getDateTo(), $viewer)); $invitees = $event->getInvitees(); $invitee_list = new PHUIStatusListView(); foreach ($invitees as $invitee) { + if ($invitee->isUninvited()) { + continue; + } $item = new PHUIStatusItemView(); $invitee_phid = $invitee->getInviteePHID(); $target = $viewer->renderHandle($invitee_phid); + $item->setNote($invitee->getStatus()); $item->setTarget($target); $invitee_list->addItem($item); } $properties->addProperty( pht('Invitees'), $invitee_list); $properties->invokeWillRenderEvent(); $properties->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $properties->addTextContent($event->getDescription()); return $properties; } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 81bd5fd59..42dd48d8f 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1,273 +1,274 @@ <?php final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO implements PhabricatorPolicyInterface, PhabricatorMarkupInterface, PhabricatorApplicationTransactionInterface, PhabricatorSubscribableInterface, PhabricatorTokenReceiverInterface, PhabricatorDestructibleInterface, PhabricatorMentionableInterface, PhabricatorFlaggableInterface { protected $name; protected $userPHID; protected $dateFrom; protected $dateTo; protected $status; protected $description; protected $isCancelled; private $invitees = self::ATTACHABLE; const STATUS_AWAY = 1; const STATUS_SPORADIC = 2; public static function initializeNewCalendarEvent(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) ->withClasses(array('PhabricatorCalendarApplication')) ->executeOne(); return id(new PhabricatorCalendarEvent()) ->setUserPHID($actor->getPHID()) - ->setIsCancelled(0); + ->setIsCancelled(0) + ->attachInvitees(array()); } private static $statusTexts = array( self::STATUS_AWAY => 'away', self::STATUS_SPORADIC => 'sporadic', ); public function setTextStatus($status) { $statuses = array_flip(self::$statusTexts); return $this->setStatus($statuses[$status]); } public function getTextStatus() { return self::$statusTexts[$this->status]; } public function getStatusOptions() { return array( self::STATUS_AWAY => pht('Away'), self::STATUS_SPORADIC => pht('Sporadic'), ); } public function getHumanStatus() { $options = $this->getStatusOptions(); return $options[$this->status]; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text', 'dateFrom' => 'epoch', 'dateTo' => 'epoch', 'status' => 'uint32', 'description' => 'text', 'isCancelled' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'userPHID_dateFrom' => array( 'columns' => array('userPHID', 'dateTo'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorCalendarEventPHIDType::TYPECONST); } public function getMonogram() { return 'E'.$this->getID(); } public function getTerseSummary(PhabricatorUser $viewer) { $until = phabricator_date($this->dateTo, $viewer); if ($this->status == PhabricatorCalendarEvent::STATUS_SPORADIC) { return pht('Sporadic until %s', $until); } else { return pht('Away until %s', $until); } } public static function getNameForStatus($value) { switch ($value) { case self::STATUS_AWAY: return pht('Away'); case self::STATUS_SPORADIC: return pht('Sporadic'); default: return pht('Unknown'); } } public function loadCurrentStatuses($user_phids) { if (!$user_phids) { return array(); } $statuses = $this->loadAllWhere( 'userPHID IN (%Ls) AND UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo', $user_phids); return mpull($statuses, null, 'getUserPHID'); } public function getInvitees() { return $this->assertAttached($this->invitees); } public function attachInvitees(array $invitees) { $this->invitees = $invitees; return $this; } /** * Validates data and throws exceptions for non-sensical status * windows */ public function save() { if ($this->getDateTo() <= $this->getDateFrom()) { throw new PhabricatorCalendarEventInvalidEpochException(); } return parent::save(); } /* -( Markup Interface )--------------------------------------------------- */ /** * @task markup */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "calendar:T{$id}:{$field}:{$hash}"; } /** * @task markup */ public function getMarkupText($field) { return $this->getDescription(); } /** * @task markup */ public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newCalendarMarkupEngine(); } /** * @task markup */ public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } /** * @task markup */ public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::getMostOpenPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getUserPHID(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorCalendarEventEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorCalendarEventTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getUserPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array($this->getUserPHID()); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEventInvitee.php b/src/applications/calendar/storage/PhabricatorCalendarEventInvitee.php index 8b7f9a063..9701a782e 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEventInvitee.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEventInvitee.php @@ -1,64 +1,72 @@ <?php final class PhabricatorCalendarEventInvitee extends PhabricatorCalendarDAO implements PhabricatorPolicyInterface { protected $eventPHID; protected $inviteePHID; protected $inviterPHID; protected $status; const STATUS_INVITED = 'invited'; const STATUS_ATTENDING = 'attending'; const STATUS_DECLINED = 'declined'; const STATUS_UNINVITED = 'uninvited'; public static function initializeNewCalendarEventInvitee( PhabricatorUser $actor, $event) { return id(new PhabricatorCalendarEventInvitee()) ->setInviterPHID($actor->getPHID()) ->setStatus(self::STATUS_INVITED) ->setEventPHID($event->getPHID()); } protected function getConfiguration() { return array( self::CONFIG_COLUMN_SCHEMA => array( 'status' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_event' => array( 'columns' => array('eventPHID', 'inviteePHID'), 'unique' => true, ), 'key_invitee' => array( 'columns' => array('inviteePHID'), ), ), ) + parent::getConfiguration(); } + public function isUninvited() { + if ($this->getStatus() == self::STATUS_UNINVITED) { + return true; + } else { + return false; + } + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::getMostOpenPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } }