diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php index 888f52c572..4f92fa5b0c 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php @@ -1,621 +1,578 @@ id; } public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $user_phid = $viewer->getPHID(); $this->id = $request->getURIData('id'); $error_name = true; $error_recurrence_end_date = null; $error_start_date = true; $error_end_date = true; $validation_exception = null; $is_recurring_id = celerity_generate_unique_node_id(); $recurrence_end_date_id = celerity_generate_unique_node_id(); $frequency_id = celerity_generate_unique_node_id(); $all_day_id = celerity_generate_unique_node_id(); $start_date_id = celerity_generate_unique_node_id(); $end_date_id = celerity_generate_unique_node_id(); $next_workflow = $request->getStr('next'); $uri_query = $request->getStr('query'); if ($this->isCreate()) { $mode = $request->getStr('mode'); $event = PhabricatorCalendarEvent::initializeNewCalendarEvent( $viewer, $mode); $create_start_year = $request->getInt('year'); $create_start_month = $request->getInt('month'); $create_start_day = $request->getInt('day'); $create_start_time = $request->getStr('time'); if ($create_start_year) { $start = AphrontFormDateControlValue::newFromParts( $viewer, $create_start_year, $create_start_month, $create_start_day, $create_start_time); if (!$start->isValid()) { return new Aphront400Response(); } $start_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $start->getEpoch()); $end = clone $start_value->getDateTime(); $end->modify('+1 hour'); $end_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $end->format('U')); } else { list($start_value, $end_value) = $this->getDefaultTimeValues($viewer); } $recurrence_end_date_value = clone $end_value; $recurrence_end_date_value->setOptional(true); $submit_label = pht('Create'); $title = pht('Create Event'); $header_icon = 'fa-plus-square'; $redirect = 'created'; $subscribers = array(); $invitees = array($user_phid); $cancel_uri = $this->getApplicationURI(); } else { $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$event) { return new Aphront404Response(); } $end_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $event->getViewerDateTo()); $start_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $event->getViewerDateFrom()); $recurrence_end_date_value = id(clone $end_value) ->setOptional(true); $submit_label = pht('Update'); $title = pht('Edit Event: %s', $event->getName()); $header_icon = 'fa-pencil'; $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $event->getPHID()); - $invitees = array(); - foreach ($event->getInvitees() as $invitee) { - if ($invitee->isUninvited()) { - continue; - } else { - $invitees[] = $invitee->getInviteePHID(); - } - } - + $invitees = $event->getInviteePHIDsForEdit(); $cancel_uri = $event->getURI(); } if ($this->isCreate()) { $projects = array(); } else { $projects = PhabricatorEdgeQuery::loadDestinationPHIDs( $event->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $projects = array_reverse($projects); } $name = $event->getName(); $description = $event->getDescription(); $is_all_day = $event->getIsAllDay(); $is_recurring = $event->getIsRecurring(); $is_parent = $event->isParentEvent(); $frequency = idx($event->getRecurrenceFrequency(), 'rule'); $icon = $event->getIcon(); $edit_policy = $event->getEditPolicy(); $view_policy = $event->getViewPolicy(); $space = $event->getSpacePHID(); if ($request->isFormPost()) { $is_all_day = $request->getStr('isAllDay'); if ($is_all_day) { // TODO: This is a very gross temporary hack to get this working // reasonably: if this is an all day event, force the viewer's // timezone to UTC so the date controls get interpreted as UTC. $viewer->overrideTimezoneIdentifier('UTC'); } $xactions = array(); $name = $request->getStr('name'); $start_value = AphrontFormDateControlValue::newFromRequest( $request, 'start'); $end_value = AphrontFormDateControlValue::newFromRequest( $request, 'end'); $recurrence_end_date_value = AphrontFormDateControlValue::newFromRequest( $request, 'recurrenceEndDate'); $recurrence_end_date_value->setOptional(true); $projects = $request->getArr('projects'); $description = $request->getStr('description'); $subscribers = $request->getArr('subscribers'); $edit_policy = $request->getStr('editPolicy'); $view_policy = $request->getStr('viewPolicy'); $space = $request->getStr('spacePHID'); $is_recurring = $request->getStr('isRecurring') ? 1 : 0; $frequency = $request->getStr('frequency'); $icon = $request->getStr('icon'); $invitees = $request->getArr('invitees'); - $new_invitees = $this->getNewInviteeList($invitees, $event); - $status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; - if ($this->isCreate()) { - $status = idx($new_invitees, $viewer->getPHID()); - if ($status) { - $new_invitees[$viewer->getPHID()] = $status_attending; - } - } $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_NAME) ->setNewValue($name); if ($is_recurring && $this->isCreate()) { $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_RECURRING) ->setNewValue($is_recurring); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_FREQUENCY) ->setNewValue(array('rule' => $frequency)); if (!$recurrence_end_date_value->isDisabled()) { $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE) ->setNewValue($recurrence_end_date_value->getEpoch()); } } if (($is_recurring && $this->isCreate()) || !$is_parent) { $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_ALL_DAY) ->setNewValue($is_all_day); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_ICON) ->setNewValue($icon); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_START_DATE) ->setNewValue($start_value->getEpoch()); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_END_DATE) ->setNewValue($end_value->getEpoch()); } $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue(array('=' => array_fuse($subscribers))); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_INVITE) - ->setNewValue($new_invitees); + ->setNewValue($invitees); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION) ->setNewValue($description); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($request->getStr('viewPolicy')); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($request->getStr('editPolicy')); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SPACE) ->setNewValue($space); $editor = id(new PhabricatorCalendarEventEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $proj_edge_type) ->setNewValue(array('=' => array_fuse($projects))); $xactions = $editor->applyTransactions($event, $xactions); $response = id(new AphrontRedirectResponse()); switch ($next_workflow) { case 'day': if (!$uri_query) { $uri_query = 'month'; } $year = $start_value->getDateTime()->format('Y'); $month = $start_value->getDateTime()->format('m'); $day = $start_value->getDateTime()->format('d'); $response->setURI( '/calendar/query/'.$uri_query.'/'.$year.'/'.$month.'/'.$day.'/'); break; default: $response->setURI('/E'.$event->getID()); break; } return $response; } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $error_name = $ex->getShortMessage( PhabricatorCalendarEventTransaction::TYPE_NAME); $error_start_date = $ex->getShortMessage( PhabricatorCalendarEventTransaction::TYPE_START_DATE); $error_end_date = $ex->getShortMessage( PhabricatorCalendarEventTransaction::TYPE_END_DATE); $error_recurrence_end_date = $ex->getShortMessage( PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE); } } $is_recurring_checkbox = null; $recurrence_end_date_control = null; $recurrence_frequency_select = null; $all_day_checkbox = null; $start_control = null; $end_control = null; $recurring_date_edit_label = null; $current_policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($event) ->execute(); $name = id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($name) ->setError($error_name); if ($this->isCreate()) { Javelin::initBehavior('recurring-edit', array( 'isRecurring' => $is_recurring_id, 'frequency' => $frequency_id, 'recurrenceEndDate' => $recurrence_end_date_id, )); $is_recurring_checkbox = id(new AphrontFormCheckboxControl()) ->addCheckbox( 'isRecurring', 1, pht('Recurring Event'), $is_recurring, $is_recurring_id); $recurrence_end_date_control = id(new AphrontFormDateControl()) ->setUser($viewer) ->setName('recurrenceEndDate') ->setLabel(pht('Recurrence End Date')) ->setError($error_recurrence_end_date) ->setValue($recurrence_end_date_value) ->setID($recurrence_end_date_id) ->setIsTimeDisabled(true) ->setIsDisabled($recurrence_end_date_value->isDisabled()) ->setAllowNull(true); $recurrence_frequency_select = id(new AphrontFormSelectControl()) ->setName('frequency') ->setOptions(array( PhabricatorCalendarEvent::FREQUENCY_DAILY => pht('Daily'), PhabricatorCalendarEvent::FREQUENCY_WEEKLY => pht('Weekly'), PhabricatorCalendarEvent::FREQUENCY_MONTHLY => pht('Monthly'), PhabricatorCalendarEvent::FREQUENCY_YEARLY => pht('Yearly'), )) ->setValue($frequency) ->setLabel(pht('Recurring Event Frequency')) ->setID($frequency_id) ->setDisabled(!$is_recurring); } if ($this->isCreate() || (!$is_parent && !$this->isCreate())) { Javelin::initBehavior('event-all-day', array( 'allDayID' => $all_day_id, 'startDateID' => $start_date_id, 'endDateID' => $end_date_id, )); $all_day_checkbox = id(new AphrontFormCheckboxControl()) ->addCheckbox( 'isAllDay', 1, pht('All Day Event'), $is_all_day, $all_day_id); $start_control = id(new AphrontFormDateControl()) ->setUser($viewer) ->setName('start') ->setLabel(pht('Start')) ->setError($error_start_date) ->setValue($start_value) ->setID($start_date_id) ->setIsTimeDisabled($is_all_day) ->setEndDateID($end_date_id); $end_control = id(new AphrontFormDateControl()) ->setUser($viewer) ->setName('end') ->setLabel(pht('End')) ->setError($error_end_date) ->setValue($end_value) ->setID($end_date_id) ->setIsTimeDisabled($is_all_day); } else if ($is_parent) { $recurring_date_edit_label = id(new AphrontFormStaticControl()) ->setUser($viewer) ->setValue(pht('Date and time of recurring event cannot be edited.')); if (!$recurrence_end_date_value->isDisabled()) { $disabled_recurrence_end_date_value = $recurrence_end_date_value->getValueAsFormat('M d, Y'); $recurrence_end_date_control = id(new AphrontFormStaticControl()) ->setUser($viewer) ->setLabel(pht('Recurrence End Date')) ->setValue($disabled_recurrence_end_date_value) ->setDisabled(true); } $recurrence_frequency_select = id(new AphrontFormSelectControl()) ->setName('frequency') ->setOptions(array( 'daily' => pht('Daily'), 'weekly' => pht('Weekly'), 'monthly' => pht('Monthly'), 'yearly' => pht('Yearly'), )) ->setValue($frequency) ->setLabel(pht('Recurring Event Frequency')) ->setID($frequency_id) ->setDisabled(true); $all_day_checkbox = id(new AphrontFormCheckboxControl()) ->addCheckbox( 'isAllDay', 1, pht('All Day Event'), $is_all_day, $all_day_id) ->setDisabled(true); $start_disabled = $start_value->getValueAsFormat('M d, Y, g:i A'); $end_disabled = $end_value->getValueAsFormat('M d, Y, g:i A'); $start_control = id(new AphrontFormStaticControl()) ->setUser($viewer) ->setLabel(pht('Start')) ->setValue($start_disabled) ->setDisabled(true); $end_control = id(new AphrontFormStaticControl()) ->setUser($viewer) ->setLabel(pht('End')) ->setValue($end_disabled); } $projects = id(new AphrontFormTokenizerControl()) ->setLabel(pht('Tags')) ->setName('projects') ->setValue($projects) ->setUser($viewer) ->setDatasource(new PhabricatorProjectDatasource()); $description = id(new PhabricatorRemarkupControl()) ->setLabel(pht('Description')) ->setName('description') ->setValue($description) ->setUser($viewer); $view_policies = id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setValue($view_policy) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($event) ->setPolicies($current_policies) ->setSpacePHID($space) ->setName('viewPolicy'); $edit_policies = id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setValue($edit_policy) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($event) ->setPolicies($current_policies) ->setName('editPolicy'); $subscribers = id(new AphrontFormTokenizerControl()) ->setLabel(pht('Subscribers')) ->setName('subscribers') ->setValue($subscribers) ->setUser($viewer) ->setDatasource(new PhabricatorMetaMTAMailableDatasource()); $invitees = id(new AphrontFormTokenizerControl()) ->setLabel(pht('Invitees')) ->setName('invitees') ->setValue($invitees) ->setUser($viewer) ->setDatasource(new PhabricatorMetaMTAMailableDatasource()); - $icon = id(new PHUIFormIconSetControl()) ->setLabel(pht('Icon')) ->setName('icon') ->setIconSet(new PhabricatorCalendarIconSet()) ->setValue($icon); $form = id(new AphrontFormView()) ->addHiddenInput('next', $next_workflow) ->addHiddenInput('query', $uri_query) ->setUser($viewer) ->appendChild($name); if ($recurring_date_edit_label) { $form->appendControl($recurring_date_edit_label); } if ($is_recurring_checkbox) { $form->appendChild($is_recurring_checkbox); } if ($recurrence_end_date_control) { $form->appendChild($recurrence_end_date_control); } if ($recurrence_frequency_select) { $form->appendControl($recurrence_frequency_select); } $form ->appendChild($all_day_checkbox) ->appendChild($start_control) ->appendChild($end_control) ->appendControl($view_policies) ->appendControl($edit_policies) ->appendControl($subscribers) ->appendControl($invitees) ->appendChild($projects) ->appendChild($description) ->appendChild($icon); if ($request->isAjax()) { return $this->newDialog() ->setTitle($title) ->setWidth(AphrontDialogView::WIDTH_FULL) ->appendForm($form) ->addCancelButton($cancel_uri) ->addSubmitButton($submit_label); } $submit = id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_label); $form->appendChild($submit); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Event')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setValidationException($validation_exception) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); if (!$this->isCreate()) { $crumbs->addTextCrumb('E'.$event->getId(), '/E'.$event->getId()); $crumb_title = pht('Edit Event'); } else { $crumb_title = pht('Create Event'); } $crumbs->addTextCrumb($crumb_title); $crumbs->setBorder(true); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setHeaderIcon($header_icon); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter($form_box); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view); } - 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_status = $event->getUserInviteStatus($phid); - 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; - } - private function getDefaultTimeValues($viewer) { $start = new DateTime('@'.time()); $start->setTimeZone($viewer->getTimeZone()); $start->setTime($start->format('H'), 0, 0); $start->modify('+1 hour'); $end = id(clone $start)->modify('+1 hour'); $start_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $start->format('U')); $end_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $end->format('U')); return array($start_value, $end_value); } } diff --git a/src/applications/calendar/editor/PhabricatorCalendarEditEngine.php b/src/applications/calendar/editor/PhabricatorCalendarEditEngine.php index 4aaab4634d..ee36c7e82e 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarEditEngine.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEditEngine.php @@ -1,107 +1,126 @@ getViewer(), $mode = null); } protected function newObjectQuery() { return new PhabricatorCalendarEventQuery(); } protected function getObjectCreateTitleText($object) { return pht('Create New Event'); } protected function getObjectEditTitleText($object) { return pht('Edit Event: %s', $object->getName()); } protected function getObjectEditShortText($object) { return $object->getMonogram(); } protected function getObjectCreateShortText() { return pht('Create Event'); } protected function getObjectName() { return pht('Event'); } protected function getObjectViewURI($object) { return $object->getURI(); } protected function getEditorURI() { return $this->getApplication()->getApplicationURI('event/editpro/'); } protected function buildCustomEditFields($object) { + $viewer = $this->getViewer(); + + if ($this->getIsCreate()) { + $invitee_phids = array($viewer->getPHID()); + } else { + $invitee_phids = $object->getInviteePHIDsForEdit(); + } + $fields = array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Name of the event.')) ->setIsRequired(true) ->setTransactionType(PhabricatorCalendarEventTransaction::TYPE_NAME) ->setConduitDescription(pht('Rename the event.')) ->setConduitTypeDescription(pht('New event name.')) ->setValue($object->getName()), id(new PhabricatorRemarkupEditField()) ->setKey('description') ->setLabel(pht('Description')) ->setDescription(pht('Description of the event.')) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION) ->setConduitDescription(pht('Update the event description.')) ->setConduitTypeDescription(pht('New event description.')) ->setValue($object->getDescription()), id(new PhabricatorBoolEditField()) ->setKey('cancelled') ->setOptions(pht('Active'), pht('Cancelled')) ->setLabel(pht('Cancelled')) ->setDescription(pht('Cancel the event.')) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_CANCEL) ->setIsConduitOnly(true) ->setConduitDescription(pht('Cancel or restore the event.')) ->setConduitTypeDescription(pht('True to cancel the event.')) ->setValue($object->getIsCancelled()), + id(new PhabricatorDatasourceEditField()) + ->setKey('inviteePHIDs') + ->setAliases(array('invite', 'invitee', 'invitees', 'inviteePHID')) + ->setLabel(pht('Invitees')) + ->setDatasource(new PhabricatorMetaMTAMailableDatasource()) + ->setTransactionType(PhabricatorCalendarEventTransaction::TYPE_INVITE) + ->setDescription(pht('Users invited to the event.')) + ->setConduitDescription(pht('Change invited users.')) + ->setConduitTypeDescription(pht('New event invitees.')) + ->setValue($invitee_phids) + ->setCommentActionLabel(pht('Change Invitees')), id(new PhabricatorIconSetEditField()) ->setKey('icon') ->setLabel(pht('Icon')) ->setIconSet(new PhabricatorCalendarIconSet()) ->setTransactionType(PhabricatorCalendarEventTransaction::TYPE_ICON) ->setDescription(pht('Event icon.')) ->setConduitDescription(pht('Change the event icon.')) ->setConduitTypeDescription(pht('New event icon.')) ->setValue($object->getIcon()), ); return $fields; } } diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php index ce886d2ed6..16e00a2873 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php @@ -1,501 +1,561 @@ requireActor(); if ($object->getIsStub()) { $this->materializeStub($object); } } private function materializeStub(PhabricatorCalendarEvent $event) { if (!$event->getIsStub()) { throw new Exception( pht('Can not materialize an event stub: this event is not a stub.')); } $actor = $this->getActor(); $event->copyFromParent($actor); $event->setIsStub(0); $invitees = $event->getParentEvent()->getInvitees(); $new_invitees = array(); foreach ($invitees as $invitee) { $invitee = id(new PhabricatorCalendarEventInvitee()) ->setEventPHID($event->getPHID()) ->setInviteePHID($invitee->getInviteePHID()) ->setInviterPHID($invitee->getInviterPHID()) ->setStatus($invitee->getStatus()) ->save(); $new_invitees[] = $invitee; } $event->save(); $event->attachInvitees($new_invitees); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorCalendarEventTransaction::TYPE_NAME; $types[] = PhabricatorCalendarEventTransaction::TYPE_START_DATE; $types[] = PhabricatorCalendarEventTransaction::TYPE_END_DATE; $types[] = PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION; $types[] = PhabricatorCalendarEventTransaction::TYPE_CANCEL; $types[] = PhabricatorCalendarEventTransaction::TYPE_INVITE; $types[] = PhabricatorCalendarEventTransaction::TYPE_ALL_DAY; $types[] = PhabricatorCalendarEventTransaction::TYPE_ICON; $types[] = PhabricatorCalendarEventTransaction::TYPE_ACCEPT; $types[] = PhabricatorCalendarEventTransaction::TYPE_DECLINE; $types[] = PhabricatorCalendarEventTransaction::TYPE_RECURRING; $types[] = PhabricatorCalendarEventTransaction::TYPE_FREQUENCY; $types[] = PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE; $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorCalendarEventTransaction::TYPE_RECURRING: return $object->getIsRecurring(); case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: return $object->getRecurrenceFrequency(); case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: return $object->getRecurrenceEndDate(); case PhabricatorCalendarEventTransaction::TYPE_NAME: return $object->getName(); case PhabricatorCalendarEventTransaction::TYPE_START_DATE: return $object->getDateFrom(); case PhabricatorCalendarEventTransaction::TYPE_END_DATE: return $object->getDateTo(); case PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION: return $object->getDescription(); case PhabricatorCalendarEventTransaction::TYPE_CANCEL: return $object->getIsCancelled(); case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY: return (int)$object->getIsAllDay(); case PhabricatorCalendarEventTransaction::TYPE_ICON: return $object->getIcon(); case PhabricatorCalendarEventTransaction::TYPE_ACCEPT: case PhabricatorCalendarEventTransaction::TYPE_DECLINE: $actor_phid = $this->getActingAsPHID(); return $object->getUserInviteStatus($actor_phid); case PhabricatorCalendarEventTransaction::TYPE_INVITE: - $map = $xaction->getNewValue(); - $phids = array_keys($map); - $invitees = mpull($object->getInvitees(), null, 'getInviteePHID'); - - $old = array(); - foreach ($phids as $phid) { - $invitee = idx($invitees, $phid); - if ($invitee) { - $old[$phid] = $invitee->getStatus(); - } else { - $old[$phid] = PhabricatorCalendarEventInvitee::STATUS_UNINVITED; - } - } - return $old; + $invitees = $object->getInvitees(); + return mpull($invitees, 'getStatus', 'getInviteePHID'); } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorCalendarEventTransaction::TYPE_RECURRING: case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: case PhabricatorCalendarEventTransaction::TYPE_NAME: case PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION: case PhabricatorCalendarEventTransaction::TYPE_CANCEL: - case PhabricatorCalendarEventTransaction::TYPE_INVITE: case PhabricatorCalendarEventTransaction::TYPE_ICON: return $xaction->getNewValue(); case PhabricatorCalendarEventTransaction::TYPE_ACCEPT: return PhabricatorCalendarEventInvitee::STATUS_ATTENDING; case PhabricatorCalendarEventTransaction::TYPE_DECLINE: return PhabricatorCalendarEventInvitee::STATUS_DECLINED; case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY: return (int)$xaction->getNewValue(); case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: case PhabricatorCalendarEventTransaction::TYPE_START_DATE: case PhabricatorCalendarEventTransaction::TYPE_END_DATE: return $xaction->getNewValue(); + case PhabricatorCalendarEventTransaction::TYPE_INVITE: + $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; + $status_uninvited = PhabricatorCalendarEventInvitee::STATUS_UNINVITED; + $status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; + + $invitees = $object->getInvitees(); + foreach ($invitees as $key => $invitee) { + if ($invitee->getStatus() == $status_uninvited) { + unset($invitees[$key]); + } + } + $invitees = mpull($invitees, null, 'getInviteePHID'); + + $new = $xaction->getNewValue(); + $new = array_fuse($new); + + $all = array_keys($invitees + $new); + $map = array(); + foreach ($all as $phid) { + $is_old = isset($invitees[$phid]); + $is_new = isset($new[$phid]); + + if ($is_old && !$is_new) { + $map[$phid] = $status_uninvited; + } else if (!$is_old && $is_new) { + $map[$phid] = $status_invited; + } + } + + // If we're creating this event and the actor is inviting themselves, + // mark them as attending. + if ($this->getIsNewObject()) { + $acting_phid = $this->getActingAsPHID(); + if (isset($map[$acting_phid])) { + $map[$acting_phid] = $status_attending; + } + } + + return $map; } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorCalendarEventTransaction::TYPE_RECURRING: return $object->setIsRecurring($xaction->getNewValue()); case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: return $object->setRecurrenceFrequency($xaction->getNewValue()); case PhabricatorCalendarEventTransaction::TYPE_NAME: $object->setName($xaction->getNewValue()); return; case PhabricatorCalendarEventTransaction::TYPE_START_DATE: $object->setDateFrom($xaction->getNewValue()); return; case PhabricatorCalendarEventTransaction::TYPE_END_DATE: $object->setDateTo($xaction->getNewValue()); return; case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: $object->setRecurrenceEndDate($xaction->getNewValue()); return; case PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION: $object->setDescription($xaction->getNewValue()); return; case PhabricatorCalendarEventTransaction::TYPE_CANCEL: $object->setIsCancelled((int)$xaction->getNewValue()); return; case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY: $object->setIsAllDay((int)$xaction->getNewValue()); return; case PhabricatorCalendarEventTransaction::TYPE_ICON: $object->setIcon($xaction->getNewValue()); return; case PhabricatorCalendarEventTransaction::TYPE_INVITE: case PhabricatorCalendarEventTransaction::TYPE_ACCEPT: case PhabricatorCalendarEventTransaction::TYPE_DECLINE: return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorCalendarEventTransaction::TYPE_RECURRING: case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: case PhabricatorCalendarEventTransaction::TYPE_NAME: case PhabricatorCalendarEventTransaction::TYPE_START_DATE: case PhabricatorCalendarEventTransaction::TYPE_END_DATE: case PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION: case PhabricatorCalendarEventTransaction::TYPE_CANCEL: case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY: case PhabricatorCalendarEventTransaction::TYPE_ICON: return; case PhabricatorCalendarEventTransaction::TYPE_INVITE: $map = $xaction->getNewValue(); $phids = array_keys($map); $invitees = $object->getInvitees(); $invitees = mpull($invitees, null, 'getInviteePHID'); foreach ($phids as $phid) { $invitee = idx($invitees, $phid); if (!$invitee) { $invitee = id(new PhabricatorCalendarEventInvitee()) ->setEventPHID($object->getPHID()) ->setInviteePHID($phid) ->setInviterPHID($this->getActingAsPHID()); $invitees[] = $invitee; } $invitee->setStatus($map[$phid]) ->save(); } $object->attachInvitees($invitees); return; case PhabricatorCalendarEventTransaction::TYPE_ACCEPT: case PhabricatorCalendarEventTransaction::TYPE_DECLINE: $acting_phid = $this->getActingAsPHID(); $invitees = $object->getInvitees(); $invitees = mpull($invitees, null, 'getInviteePHID'); $invitee = idx($invitees, $acting_phid); if (!$invitee) { $invitee = id(new PhabricatorCalendarEventInvitee()) ->setEventPHID($object->getPHID()) ->setInviteePHID($acting_phid) ->setInviterPHID($acting_phid); $invitees[$acting_phid] = $invitee; } $invitee ->setStatus($xaction->getNewValue()) ->save(); $object->attachInvitees($invitees); return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { // Clear the availability caches for users whose availability is affected // by this edit. $invalidate_all = false; $invalidate_phids = array(); foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorCalendarEventTransaction::TYPE_ICON: break; case PhabricatorCalendarEventTransaction::TYPE_RECURRING: case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: case PhabricatorCalendarEventTransaction::TYPE_START_DATE: case PhabricatorCalendarEventTransaction::TYPE_END_DATE: case PhabricatorCalendarEventTransaction::TYPE_CANCEL: case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY: // For these kinds of changes, we need to invalidate the availabilty // caches for all attendees. $invalidate_all = true; break; case PhabricatorCalendarEventTransaction::TYPE_ACCEPT: case PhabricatorCalendarEventTransaction::TYPE_DECLINE: $acting_phid = $this->getActingAsPHID(); $invalidate_phids[$acting_phid] = $acting_phid; break; case PhabricatorCalendarEventTransaction::TYPE_INVITE: foreach ($xaction->getNewValue() as $phid => $ignored) { $invalidate_phids[$phid] = $phid; } break; } } $phids = mpull($object->getInvitees(), 'getInviteePHID'); $phids = array_fuse($phids); if (!$invalidate_all) { $phids = array_select_keys($phids, $invalidate_phids); } if ($phids) { $object->applyViewerTimezone($this->getActor()); $user = new PhabricatorUser(); $conn_w = $user->establishConnection('w'); queryfx( $conn_w, 'UPDATE %T SET availabilityCacheTTL = NULL WHERE phid IN (%Ls) AND availabilityCacheTTL >= %d', $user->getTableName(), $phids, $object->getDateFromForCache()); } return $xactions; } protected function validateAllTransactions( PhabricatorLiskDAO $object, array $xactions) { $start_date_xaction = PhabricatorCalendarEventTransaction::TYPE_START_DATE; $end_date_xaction = PhabricatorCalendarEventTransaction::TYPE_END_DATE; $is_recurrence_xaction = PhabricatorCalendarEventTransaction::TYPE_RECURRING; $recurrence_end_xaction = PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE; $start_date = $object->getDateFrom(); $end_date = $object->getDateTo(); $recurrence_end = $object->getRecurrenceEndDate(); $is_recurring = $object->getIsRecurring(); $errors = array(); foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $start_date_xaction) { $start_date = $xaction->getNewValue(); } else if ($xaction->getTransactionType() == $end_date_xaction) { $end_date = $xaction->getNewValue(); } else if ($xaction->getTransactionType() == $recurrence_end_xaction) { $recurrence_end = $xaction->getNewValue(); } else if ($xaction->getTransactionType() == $is_recurrence_xaction) { $is_recurring = $xaction->getNewValue(); } } if ($start_date > $end_date) { $type = PhabricatorCalendarEventTransaction::TYPE_END_DATE; $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht('End date must be after start date.'), null); } if ($recurrence_end && !$is_recurring) { $type = PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE; $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht('Event must be recurring to have a recurrence end date.'). null); } return $errors; } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); switch ($type) { case PhabricatorCalendarEventTransaction::TYPE_NAME: $missing = $this->validateIsEmptyTextField( $object->getName(), $xactions); if ($missing) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), pht('Event name is required.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } break; + case PhabricatorCalendarEventTransaction::TYPE_INVITE: + $old = $object->getInvitees(); + $old = mpull($old, null, 'getInviteePHID'); + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + $new = array_fuse($new); + $add = array_diff_key($new, $old); + if (!$add) { + continue; + } + + // In the UI, we only allow you to invite mailable objects, but there + // is no definitive marker for "invitable object" today. Just allow + // any valid object to be invited. + $objects = id(new PhabricatorObjectQuery()) + ->setViewer($this->getActor()) + ->withPHIDs($add) + ->execute(); + $objects = mpull($objects, null, 'getPHID'); + foreach ($add as $phid) { + if (isset($objects[$phid])) { + continue; + } + + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht( + 'Invitee "%s" identifies an object that does not exist or '. + 'which you do not have permission to view.', + $phid)); + } + } + break; } return $errors; } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function supportsSearch() { return true; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function getMailSubjectPrefix() { return pht('[Calendar]'); } protected function getMailTo(PhabricatorLiskDAO $object) { $phids = array(); if ($object->getUserPHID()) { $phids[] = $object->getUserPHID(); } $phids[] = $this->getActingAsPHID(); $invitees = $object->getInvitees(); foreach ($invitees as $invitee) { $status = $invitee->getStatus(); if ($status === PhabricatorCalendarEventInvitee::STATUS_ATTENDING || $status === PhabricatorCalendarEventInvitee::STATUS_INVITED) { $phids[] = $invitee->getInviteePHID(); } } $phids = array_unique($phids); return $phids; } public function getMailTagsMap() { return array( PhabricatorCalendarEventTransaction::MAILTAG_CONTENT => pht( "An event's name, status, invite list, ". "icon, and description changes."), PhabricatorCalendarEventTransaction::MAILTAG_RESCHEDULE => pht( "An event's start and end date ". "and cancellation status changes."), PhabricatorCalendarEventTransaction::MAILTAG_OTHER => pht('Other event activity not listed above occurs.'), ); } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new PhabricatorCalendarReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $name = $object->getName(); return id(new PhabricatorMetaMTAMail()) ->setSubject("E{$id}: {$name}") ->addHeader('Thread-Topic', "E{$id}: ".$object->getName()); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $description = $object->getDescription(); $body = parent::buildMailBody($object, $xactions); if (strlen($description)) { $body->addRemarkupSection( pht('EVENT DESCRIPTION'), $description); } $body->addLinkSection( pht('EVENT DETAIL'), PhabricatorEnv::getProductionURI('/E'.$object->getID())); return $body; } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 991bfbf521..3fba3f2e66 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1,610 +1,623 @@ setViewer($actor) ->withClasses(array('PhabricatorCalendarApplication')) ->executeOne(); $view_policy = null; $is_recurring = 0; if ($mode == 'public') { $view_policy = PhabricatorPolicies::getMostOpenPolicy(); } if ($mode == 'recurring') { $is_recurring = true; } return id(new PhabricatorCalendarEvent()) ->setUserPHID($actor->getPHID()) ->setIsCancelled(0) ->setIsAllDay(0) ->setIsStub(0) ->setIsRecurring($is_recurring) ->setIcon(self::DEFAULT_ICON) ->setViewPolicy($view_policy) ->setEditPolicy($actor->getPHID()) ->setSpacePHID($actor->getDefaultSpacePHID()) ->attachInvitees(array()) ->applyViewerTimezone($actor); } private function newChild(PhabricatorUser $actor, $sequence) { if (!$this->isParentEvent()) { throw new Exception( pht( 'Unable to generate a new child event for an event which is not '. 'a recurring parent event!')); } $child = id(new self()) ->setIsCancelled(0) ->setIsStub(0) ->setInstanceOfEventPHID($this->getPHID()) ->setSequenceIndex($sequence) ->setIsRecurring(true) ->setRecurrenceFrequency($this->getRecurrenceFrequency()) ->attachParentEvent($this); return $child->copyFromParent($actor); } protected function readField($field) { static $inherit = array( 'userPHID' => true, 'isAllDay' => true, 'icon' => true, 'spacePHID' => true, 'viewPolicy' => true, 'editPolicy' => true, 'name' => true, 'description' => true, ); // Read these fields from the parent event instead of this event. For // example, we want any changes to the parent event's name to if (isset($inherit[$field])) { if ($this->getIsStub()) { // TODO: This should be unconditional, but the execution order of // CalendarEventQuery and applyViewerTimezone() are currently odd. if ($this->parentEvent !== self::ATTACHABLE) { return $this->getParentEvent()->readField($field); } } } return parent::readField($field); } public function copyFromParent(PhabricatorUser $actor) { if (!$this->isChildEvent()) { throw new Exception( pht( 'Unable to copy from parent event: this is not a child event.')); } $parent = $this->getParentEvent(); $this ->setUserPHID($parent->getUserPHID()) ->setIsAllDay($parent->getIsAllDay()) ->setIcon($parent->getIcon()) ->setSpacePHID($parent->getSpacePHID()) ->setViewPolicy($parent->getViewPolicy()) ->setEditPolicy($parent->getEditPolicy()) ->setName($parent->getName()) ->setDescription($parent->getDescription()); $frequency = $parent->getFrequencyUnit(); $modify_key = '+'.$this->getSequenceIndex().' '.$frequency; $date = $parent->getDateFrom(); $date_time = PhabricatorTime::getDateTimeFromEpoch($date, $actor); $date_time->modify($modify_key); $date = $date_time->format('U'); $duration = $this->getDuration(); $this ->setDateFrom($date) ->setDateTo($date + $duration); return $this; } public function newStub(PhabricatorUser $actor, $sequence) { $stub = $this->newChild($actor, $sequence); $stub->setIsStub(1); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $stub->save(); unset($unguarded); $stub->applyViewerTimezone($actor); return $stub; } public function newGhost(PhabricatorUser $actor, $sequence) { $ghost = $this->newChild($actor, $sequence); $ghost ->setIsGhostEvent(true) ->makeEphemeral(); $ghost->applyViewerTimezone($actor); return $ghost; } public function getViewerDateFrom() { if ($this->viewerDateFrom === null) { throw new PhutilInvalidStateException('applyViewerTimezone'); } return $this->viewerDateFrom; } public function getViewerDateTo() { if ($this->viewerDateTo === null) { throw new PhutilInvalidStateException('applyViewerTimezone'); } return $this->viewerDateTo; } public function applyViewerTimezone(PhabricatorUser $viewer) { if (!$this->getIsAllDay()) { $this->viewerDateFrom = $this->getDateFrom(); $this->viewerDateTo = $this->getDateTo(); } else { $zone = $viewer->getTimeZone(); $this->viewerDateFrom = $this->getDateEpochForTimeZone( $this->getDateFrom(), new DateTimeZone('UTC'), 'Y-m-d', null, $zone); $this->viewerDateTo = $this->getDateEpochForTimeZone( $this->getDateTo(), new DateTimeZone('UTC'), 'Y-m-d 23:59:00', null, $zone); } return $this; } public function getDuration() { return $this->getDateTo() - $this->getDateFrom(); } private function getDateEpochForTimeZone( $epoch, $src_zone, $format, $adjust, $dst_zone) { $src = new DateTime('@'.$epoch); $src->setTimeZone($src_zone); if (strlen($adjust)) { $adjust = ' '.$adjust; } $dst = new DateTime($src->format($format).$adjust, $dst_zone); return $dst->format('U'); } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } return parent::save(); } /** * Get the event start epoch for evaluating invitee availability. * * When assessing availability, we pretend events start earlier than they * really do. This allows us to mark users away for the entire duration of a * series of back-to-back meetings, even if they don't strictly overlap. * * @return int Event start date for availability caches. */ public function getDateFromForCache() { return ($this->getViewerDateFrom() - phutil_units('15 minutes in seconds')); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text', 'dateFrom' => 'epoch', 'dateTo' => 'epoch', 'description' => 'text', 'isCancelled' => 'bool', 'isAllDay' => 'bool', 'icon' => 'text32', 'mailKey' => 'bytes20', 'isRecurring' => 'bool', 'recurrenceEndDate' => 'epoch?', 'instanceOfEventPHID' => 'phid?', 'sequenceIndex' => 'uint32?', 'isStub' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'userPHID_dateFrom' => array( 'columns' => array('userPHID', 'dateTo'), ), 'key_instance' => array( 'columns' => array('instanceOfEventPHID', 'sequenceIndex'), 'unique' => true, ), ), self::CONFIG_SERIALIZATION => array( 'recurrenceFrequency' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorCalendarEventPHIDType::TYPECONST); } public function getMonogram() { return 'E'.$this->getID(); } public function getInvitees() { return $this->assertAttached($this->invitees); } public function attachInvitees(array $invitees) { $this->invitees = $invitees; return $this; } + public function getInviteePHIDsForEdit() { + $invitees = array(); + + foreach ($this->getInvitees() as $invitee) { + if ($invitee->isUninvited()) { + continue; + } + $invitees[] = $invitee->getInviteePHID(); + } + + return $invitees; + } + public function getUserInviteStatus($phid) { $invitees = $this->getInvitees(); $invitees = mpull($invitees, null, 'getInviteePHID'); $invited = idx($invitees, $phid); if (!$invited) { return PhabricatorCalendarEventInvitee::STATUS_UNINVITED; } $invited = $invited->getStatus(); return $invited; } public function getIsUserAttending($phid) { $attending_status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; $old_status = $this->getUserInviteStatus($phid); $is_attending = ($old_status == $attending_status); return $is_attending; } public function getIsUserInvited($phid) { $uninvited_status = PhabricatorCalendarEventInvitee::STATUS_UNINVITED; $declined_status = PhabricatorCalendarEventInvitee::STATUS_DECLINED; $status = $this->getUserInviteStatus($phid); if ($status == $uninvited_status || $status == $declined_status) { return false; } return true; } public function getIsGhostEvent() { return $this->isGhostEvent; } public function setIsGhostEvent($is_ghost_event) { $this->isGhostEvent = $is_ghost_event; return $this; } public function getFrequencyUnit() { $frequency = idx($this->recurrenceFrequency, 'rule'); switch ($frequency) { case 'daily': return 'day'; case 'weekly': return 'week'; case 'monthly': return 'month'; case 'yearly': return 'year'; default: return 'day'; } } public function getURI() { if ($this->getIsGhostEvent()) { $base = $this->getParentEvent()->getURI(); $sequence = $this->getSequenceIndex(); return "{$base}/{$sequence}/"; } return '/'.$this->getMonogram(); } public function getParentEvent() { return $this->assertAttached($this->parentEvent); } public function attachParentEvent($event) { $this->parentEvent = $event; return $this; } public function isParentEvent() { return ($this->isRecurring && !$this->instanceOfEventPHID); } public function isChildEvent() { return ($this->instanceOfEventPHID !== null); } public function isCancelledEvent() { if ($this->getIsCancelled()) { return true; } if ($this->isChildEvent()) { if ($this->getParentEvent()->getIsCancelled()) { return true; } } return false; } public function getDisplayDuration() { $seconds = $this->getDuration(); $minutes = round($seconds / 60, 1); $hours = round($minutes / 60, 3); $days = round($hours / 24, 2); $duration = ''; if ($days >= 1) { return pht( '%s day(s)', round($days, 1)); } else if ($hours >= 1) { return pht( '%s hour(s)', round($hours, 1)); } else if ($minutes >= 1) { return pht( '%s minute(s)', round($minutes, 0)); } } /* -( 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 $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { // The owner of a task can always view and edit it. $user_phid = $this->getUserPHID(); if ($user_phid) { $viewer_phid = $viewer->getPHID(); if ($viewer_phid == $user_phid) { return true; } } if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { $status = $this->getUserInviteStatus($viewer->getPHID()); if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED || $status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING || $status == PhabricatorCalendarEventInvitee::STATUS_DECLINED) { return true; } } return false; } public function describeAutomaticCapability($capability) { return pht('The owner of an event can always view and edit it, and invitees can always view it, except if the event is an instance of a recurring event.'); } /* -( 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()); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array($this->getUserPHID()); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhabricatorCalendarEventFulltextEngine(); } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php b/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php index 584c41e569..0040ff8817 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php @@ -1,578 +1,585 @@ getTransactionType()) { case self::TYPE_NAME: case self::TYPE_START_DATE: case self::TYPE_END_DATE: case self::TYPE_DESCRIPTION: case self::TYPE_CANCEL: case self::TYPE_ALL_DAY: case self::TYPE_RECURRING: case self::TYPE_FREQUENCY: case self::TYPE_RECURRENCE_END_DATE: $phids[] = $this->getObjectPHID(); break; case self::TYPE_INVITE: $new = $this->getNewValue(); foreach ($new as $phid => $status) { $phids[] = $phid; } break; } return $phids; } public function shouldHide() { $old = $this->getOldValue(); switch ($this->getTransactionType()) { case self::TYPE_START_DATE: case self::TYPE_END_DATE: case self::TYPE_DESCRIPTION: case self::TYPE_CANCEL: case self::TYPE_ALL_DAY: case self::TYPE_INVITE: case self::TYPE_RECURRING: case self::TYPE_FREQUENCY: case self::TYPE_RECURRENCE_END_DATE: return ($old === null); } return parent::shouldHide(); } public function getIcon() { switch ($this->getTransactionType()) { case self::TYPE_ICON: return $this->getNewValue(); case self::TYPE_NAME: case self::TYPE_START_DATE: case self::TYPE_END_DATE: case self::TYPE_DESCRIPTION: case self::TYPE_ALL_DAY: case self::TYPE_CANCEL: case self::TYPE_RECURRING: case self::TYPE_FREQUENCY: case self::TYPE_RECURRENCE_END_DATE: return 'fa-pencil'; break; case self::TYPE_INVITE: return 'fa-user-plus'; break; } return parent::getIcon(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { case self::TYPE_NAME: if ($old === null) { return pht( '%s created this event.', $this->renderHandleLink($author_phid)); } else { return pht( '%s changed the name of this event from %s to %s.', $this->renderHandleLink($author_phid), $old, $new); } case self::TYPE_START_DATE: if ($old) { return pht( '%s edited the start date of this event.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_END_DATE: if ($old) { return pht( '%s edited the end date of this event.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_DESCRIPTION: return pht( "%s updated the event's description.", $this->renderHandleLink($author_phid)); case self::TYPE_ALL_DAY: if ($new) { return pht( '%s made this an all day event.', $this->renderHandleLink($author_phid)); } else { return pht( '%s converted this from an all day event.', $this->renderHandleLink($author_phid)); } case self::TYPE_ICON: $set = new PhabricatorCalendarIconSet(); return pht( '%s set this event\'s icon to %s.', $this->renderHandleLink($author_phid), $set->getIconLabel($new)); break; case self::TYPE_CANCEL: if ($new) { return pht( '%s cancelled this event.', $this->renderHandleLink($author_phid)); } else { return pht( '%s reinstated this event.', $this->renderHandleLink($author_phid)); } case self::TYPE_ACCEPT: return pht( '%s is attending this event.', $this->renderHandleLink($author_phid)); case self::TYPE_DECLINE: return pht( '%s declined this event.', $this->renderHandleLink($author_phid)); case self::TYPE_INVITE: $text = null; + // Fill in any new invitees as "uninvited" in the old data, to make + // some of the rendering logic a little easier. + $status_uninvited = PhabricatorCalendarEventInvitee::STATUS_UNINVITED; + $old = $old + array_fill_keys(array_keys($new), $status_uninvited); + if (count($old) === 1 && count($new) === 1 && isset($old[$author_phid])) { // user joined/declined/accepted event themself $old_status = $old[$author_phid]; $new_status = $new[$author_phid]; if ($old_status !== $new_status) { switch ($new_status) { case PhabricatorCalendarEventInvitee::STATUS_INVITED: $text = pht( '%s has joined this event.', $this->renderHandleLink($author_phid)); break; case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: $text = pht( '%s is attending this event.', $this->renderHandleLink($author_phid)); break; case PhabricatorCalendarEventInvitee::STATUS_DECLINED: case PhabricatorCalendarEventInvitee::STATUS_UNINVITED: $text = pht( '%s has declined this event.', $this->renderHandleLink($author_phid)); break; default: $text = pht( '%s has changed their status for this event.', $this->renderHandleLink($author_phid)); break; } } } else { // user changed status for many users $self_joined = null; $self_declined = null; $added = array(); $uninvited = array(); foreach ($new as $phid => $status) { if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED || $status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING) { // added users $added[] = $phid; } else if ( $status == PhabricatorCalendarEventInvitee::STATUS_DECLINED || $status == PhabricatorCalendarEventInvitee::STATUS_UNINVITED) { $uninvited[] = $phid; } } $count_added = count($added); $count_uninvited = count($uninvited); $added_text = null; $uninvited_text = null; if ($count_added > 0 && $count_uninvited == 0) { $added_text = $this->renderHandleList($added); $text = pht('%s invited %s.', $this->renderHandleLink($author_phid), $added_text); } else if ($count_added > 0 && $count_uninvited > 0) { $added_text = $this->renderHandleList($added); $uninvited_text = $this->renderHandleList($uninvited); $text = pht('%s invited %s and uninvited %s.', $this->renderHandleLink($author_phid), $added_text, $uninvited_text); } else if ($count_added == 0 && $count_uninvited > 0) { $uninvited_text = $this->renderHandleList($uninvited); $text = pht('%s uninvited %s.', $this->renderHandleLink($author_phid), $uninvited_text); } else { $text = pht('%s updated the invitee list.', $this->renderHandleLink($author_phid)); } } return $text; case self::TYPE_RECURRING: $text = pht('%s made this event recurring.', $this->renderHandleLink($author_phid)); return $text; case self::TYPE_FREQUENCY: $text = ''; switch ($new['rule']) { case PhabricatorCalendarEvent::FREQUENCY_DAILY: $text = pht('%s set this event to repeat daily.', $this->renderHandleLink($author_phid)); break; case PhabricatorCalendarEvent::FREQUENCY_WEEKLY: $text = pht('%s set this event to repeat weekly.', $this->renderHandleLink($author_phid)); break; case PhabricatorCalendarEvent::FREQUENCY_MONTHLY: $text = pht('%s set this event to repeat monthly.', $this->renderHandleLink($author_phid)); break; case PhabricatorCalendarEvent::FREQUENCY_YEARLY: $text = pht('%s set this event to repeat yearly.', $this->renderHandleLink($author_phid)); break; } return $text; case self::TYPE_RECURRENCE_END_DATE: $text = pht('%s has changed the recurrence end date of this event.', $this->renderHandleLink($author_phid)); return $text; } return parent::getTitle(); } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $viewer = $this->getViewer(); $type = $this->getTransactionType(); switch ($type) { case self::TYPE_NAME: if ($old === null) { return pht( '%s created %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s changed the name of %s from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old, $new); } case self::TYPE_START_DATE: if ($old) { $old = phabricator_datetime($old, $viewer); $new = phabricator_datetime($new, $viewer); return pht( '%s changed the start date of %s from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old, $new); } break; case self::TYPE_END_DATE: if ($old) { $old = phabricator_datetime($old, $viewer); $new = phabricator_datetime($new, $viewer); return pht( '%s edited the end date of %s from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old, $new); } break; case self::TYPE_DESCRIPTION: return pht( '%s updated the description of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case self::TYPE_ALL_DAY: if ($new) { return pht( '%s made %s an all day event.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s converted %s from an all day event.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } case self::TYPE_ICON: $set = new PhabricatorCalendarIconSet(); return pht( '%s set the icon for %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $set->getIconLabel($new)); case self::TYPE_CANCEL: if ($new) { return pht( '%s cancelled %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s reinstated %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } case self::TYPE_ACCEPT: return pht( '%s is attending %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case self::TYPE_DECLINE: return pht( '%s declined %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case self::TYPE_INVITE: $text = null; + $status_uninvited = PhabricatorCalendarEventInvitee::STATUS_UNINVITED; + $old = $old + array_fill_keys(array_keys($new), $status_uninvited); + if (count($old) === 1 && count($new) === 1 && isset($old[$author_phid])) { // user joined/declined/accepted event themself $old_status = $old[$author_phid]; $new_status = $new[$author_phid]; if ($old_status !== $new_status) { switch ($new_status) { case PhabricatorCalendarEventInvitee::STATUS_INVITED: $text = pht( '%s has joined %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: $text = pht( '%s is attending %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; case PhabricatorCalendarEventInvitee::STATUS_DECLINED: case PhabricatorCalendarEventInvitee::STATUS_UNINVITED: $text = pht( '%s has declined %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; default: $text = pht( '%s has changed their status of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; } } } else { // user changed status for many users $self_joined = null; $self_declined = null; $added = array(); $uninvited = array(); foreach ($new as $phid => $status) { if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED || $status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING) { // added users $added[] = $phid; } else if ( $status == PhabricatorCalendarEventInvitee::STATUS_DECLINED || $status == PhabricatorCalendarEventInvitee::STATUS_UNINVITED) { $uninvited[] = $phid; } } $count_added = count($added); $count_uninvited = count($uninvited); $added_text = null; $uninvited_text = null; if ($count_added > 0 && $count_uninvited == 0) { $added_text = $this->renderHandleList($added); $text = pht('%s invited %s to %s.', $this->renderHandleLink($author_phid), $added_text, $this->renderHandleLink($object_phid)); } else if ($count_added > 0 && $count_uninvited > 0) { $added_text = $this->renderHandleList($added); $uninvited_text = $this->renderHandleList($uninvited); $text = pht('%s invited %s and uninvited %s to %s.', $this->renderHandleLink($author_phid), $added_text, $uninvited_text, $this->renderHandleLink($object_phid)); } else if ($count_added == 0 && $count_uninvited > 0) { $uninvited_text = $this->renderHandleList($uninvited); $text = pht('%s uninvited %s to %s.', $this->renderHandleLink($author_phid), $uninvited_text, $this->renderHandleLink($object_phid)); } else { $text = pht('%s updated the invitee list of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } } return $text; case self::TYPE_RECURRING: $text = pht('%s made %s a recurring event.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); return $text; case self::TYPE_FREQUENCY: $text = ''; switch ($new['rule']) { case PhabricatorCalendarEvent::FREQUENCY_DAILY: $text = pht('%s set %s to repeat daily.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; case PhabricatorCalendarEvent::FREQUENCY_WEEKLY: $text = pht('%s set %s to repeat weekly.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; case PhabricatorCalendarEvent::FREQUENCY_MONTHLY: $text = pht('%s set %s to repeat monthly.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; case PhabricatorCalendarEvent::FREQUENCY_YEARLY: $text = pht('%s set %s to repeat yearly.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; } return $text; case self::TYPE_RECURRENCE_END_DATE: $text = pht('%s set the recurrence end date of %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $new); return $text; } return parent::getTitleForFeed(); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_NAME: case self::TYPE_START_DATE: case self::TYPE_END_DATE: case self::TYPE_DESCRIPTION: case self::TYPE_CANCEL: case self::TYPE_INVITE: return PhabricatorTransactions::COLOR_GREEN; } return parent::getColor(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: return ($this->getOldValue() !== null); } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: $old = $this->getOldValue(); $new = $this->getNewValue(); return $this->renderTextCorpusChangeDetails( $viewer, $old, $new); } return parent::renderChangeDetails($viewer); } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { case self::TYPE_NAME: case self::TYPE_DESCRIPTION: case self::TYPE_INVITE: case self::TYPE_ICON: $tags[] = self::MAILTAG_CONTENT; break; case self::TYPE_START_DATE: case self::TYPE_END_DATE: case self::TYPE_CANCEL: $tags[] = self::MAILTAG_RESCHEDULE; break; } return $tags; } }