diff --git a/resources/sql/autopatches/20161005.cal.01.rrules.php b/resources/sql/autopatches/20161005.cal.01.rrules.php new file mode 100644 index 0000000000..e2e61ba30a --- /dev/null +++ b/resources/sql/autopatches/20161005.cal.01.rrules.php @@ -0,0 +1,44 @@ +establishConnection('w'); +$table_name = 'calendar_event'; + +foreach (new LiskRawMigrationIterator($conn, $table_name) as $row) { + $parameters = phutil_json_decode($row['parameters']); + if (isset($parameters['recurrenceRule'])) { + // This event has already been migrated. + continue; + } + + if (!$row['isRecurring']) { + continue; + } + + $old_rule = $row['recurrenceFrequency']; + if (!$old_rule) { + continue; + } + + try { + $frequency = phutil_json_decode($old_rule); + if ($frequency) { + $frequency_rule = $frequency['rule']; + $frequency_rule = phutil_utf8_strtoupper($frequency_rule); + + $rrule = id(new PhutilCalendarRecurrenceRule()) + ->setFrequency($frequency_rule); + } + } catch (Exception $ex) { + continue; + } + + $parameters['recurrenceRule'] = $rrule->toDictionary(); + + queryfx( + $conn, + 'UPDATE %T SET parameters = %s WHERE id = %d', + $table_name, + phutil_json_encode($parameters), + $row['id']); +} diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php index 8f7ee8929e..2a1e08ae89 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php @@ -1,530 +1,537 @@ getViewer(); $event = $this->loadEvent(); if (!$event) { return new Aphront404Response(); } // If we looked up or generated a stub event, redirect to that event's // canonical URI. $id = $request->getURIData('id'); if ($event->getID() != $id) { $uri = $event->getURI(); return id(new AphrontRedirectResponse())->setURI($uri); } $monogram = $event->getMonogram(); $page_title = $monogram.' '.$event->getName(); $crumbs = $this->buildApplicationCrumbs(); $start = $event->newStartDateTime() ->newPHPDateTime(); $crumbs->addTextCrumb( $start->format('F Y'), '/calendar/query/month/'.$start->format('Y/m/')); $crumbs->addTextCrumb( $start->format('D jS'), '/calendar/query/month/'.$start->format('Y/m/d/')); $crumbs->addTextCrumb($monogram); $crumbs->setBorder(true); $timeline = $this->buildTransactionTimeline( $event, new PhabricatorCalendarEventTransactionQuery()); $header = $this->buildHeaderView($event); $subheader = $this->buildSubheaderView($event); $curtain = $this->buildCurtain($event); $details = $this->buildPropertySection($event); $recurring = $this->buildRecurringSection($event); $description = $this->buildDescriptionView($event); $comment_view = id(new PhabricatorCalendarEventEditEngine()) ->setViewer($viewer) ->buildEditEngineCommentView($event); $timeline->setQuoteRef($monogram); $comment_view->setTransactionTimeline($timeline); $details_header = id(new PHUIHeaderView()) ->setHeader(pht('Details')); $recurring_header = $this->buildRecurringHeader($event); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) ->setMainColumn( array( $timeline, $comment_view, )) ->setCurtain($curtain) ->addPropertySection($details_header, $details) ->addPropertySection($recurring_header, $recurring) ->addPropertySection(pht('Description'), $description); return $this->newPage() ->setTitle($page_title) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($event->getPHID())) ->appendChild($view); } private function buildHeaderView( PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); $id = $event->getID(); if ($event->isCancelledEvent()) { $icon = 'fa-ban'; $color = 'red'; $status = pht('Cancelled'); } else { $icon = 'fa-check'; $color = 'bluegrey'; $status = pht('Active'); } $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($event->getName()) ->setStatus($icon, $color, $status) ->setPolicyObject($event) ->setHeaderIcon($event->getIcon()); foreach ($this->buildRSVPActions($event) as $action) { $header->addActionLink($action); } return $header; } private function buildCurtain(PhabricatorCalendarEvent $event) { $viewer = $this->getRequest()->getUser(); $id = $event->getID(); $is_attending = $event->getIsUserAttending($viewer->getPHID()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $event, PhabricatorPolicyCapability::CAN_EDIT); $edit_uri = "event/edit/{$id}/"; if ($event->isChildEvent()) { $edit_label = pht('Edit This Instance'); } else { $edit_label = pht('Edit Event'); } $curtain = $this->newCurtainView($event); if ($edit_label && $edit_uri) { $curtain->addAction( id(new PhabricatorActionView()) ->setName($edit_label) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI($edit_uri)) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); } if ($is_attending) { $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Decline Event')) ->setIcon('fa-user-times') ->setHref($this->getApplicationURI("event/join/{$id}/")) ->setWorkflow(true)); } else { $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Join Event')) ->setIcon('fa-user-plus') ->setHref($this->getApplicationURI("event/join/{$id}/")) ->setWorkflow(true)); } $cancel_uri = $this->getApplicationURI("event/cancel/{$id}/"); $cancel_disabled = !$can_edit; if ($event->isChildEvent()) { $cancel_label = pht('Cancel This Instance'); $reinstate_label = pht('Reinstate This Instance'); if ($event->getParentEvent()->getIsCancelled()) { $cancel_disabled = true; } } else if ($event->isParentEvent()) { $cancel_label = pht('Cancel All'); $reinstate_label = pht('Reinstate All'); } else { $cancel_label = pht('Cancel Event'); $reinstate_label = pht('Reinstate Event'); } if ($event->isCancelledEvent()) { $curtain->addAction( id(new PhabricatorActionView()) ->setName($reinstate_label) ->setIcon('fa-plus') ->setHref($cancel_uri) ->setDisabled($cancel_disabled) ->setWorkflow(true)); } else { $curtain->addAction( id(new PhabricatorActionView()) ->setName($cancel_label) ->setIcon('fa-times') ->setHref($cancel_uri) ->setDisabled($cancel_disabled) ->setWorkflow(true)); } $ics_name = $event->getICSFilename(); $export_uri = $this->getApplicationURI("event/export/{$id}/{$ics_name}"); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Export as .ics')) ->setIcon('fa-download') ->setHref($export_uri)); return $curtain; } private function buildPropertySection( PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer); $invitees = $event->getInvitees(); foreach ($invitees as $key => $invitee) { if ($invitee->isUninvited()) { unset($invitees[$key]); } } if ($invitees) { $invitee_list = new PHUIStatusListView(); $icon_invited = PHUIStatusItemView::ICON_OPEN; $icon_attending = PHUIStatusItemView::ICON_ACCEPT; $icon_declined = PHUIStatusItemView::ICON_REJECT; $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; $status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; $status_declined = PhabricatorCalendarEventInvitee::STATUS_DECLINED; $icon_map = array( $status_invited => $icon_invited, $status_attending => $icon_attending, $status_declined => $icon_declined, ); $icon_color_map = array( $status_invited => null, $status_attending => 'green', $status_declined => 'red', ); foreach ($invitees as $invitee) { $item = new PHUIStatusItemView(); $invitee_phid = $invitee->getInviteePHID(); $status = $invitee->getStatus(); $target = $viewer->renderHandle($invitee_phid); $icon = $icon_map[$status]; $icon_color = $icon_color_map[$status]; $item->setIcon($icon, $icon_color) ->setTarget($target); $invitee_list->addItem($item); } } else { $invitee_list = phutil_tag( 'em', array(), pht('None')); } $properties->addProperty( pht('Invitees'), $invitee_list); $properties->invokeWillRenderEvent(); return $properties; } private function buildRecurringHeader(PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); if (!$event->getIsRecurring()) { return null; } $header = id(new PHUIHeaderView()) ->setHeader(pht('Recurring Event')); $sequence = $event->getSequenceIndex(); if ($event->isParentEvent()) { $parent = $event; } else { $parent = $event->getParentEvent(); } if ($parent->isValidSequenceIndex($viewer, $sequence + 1)) { $next_uri = $parent->getURI().'/'.($sequence + 1); $has_next = true; } else { $next_uri = null; $has_next = false; } if ($sequence) { if ($sequence > 1) { $previous_uri = $parent->getURI().'/'.($sequence - 1); } else { $previous_uri = $parent->getURI(); } $has_previous = true; } else { $has_previous = false; $previous_uri = null; } $prev_button = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-chevron-left') ->setHref($previous_uri) ->setDisabled(!$has_previous) ->setText(pht('Previous')); $next_button = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-chevron-right') ->setHref($next_uri) ->setDisabled(!$has_next) ->setText(pht('Next')); $header ->addActionLink($next_button) ->addActionLink($prev_button); return $header; } private function buildRecurringSection(PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); if (!$event->getIsRecurring()) { return null; } $properties = id(new PHUIPropertyListView()) ->setUser($viewer); $is_parent = $event->isParentEvent(); if ($is_parent) { $parent_link = null; } else { $parent = $event->getParentEvent(); $parent_link = $viewer ->renderHandle($parent->getPHID()) ->render(); } - $rule = $event->getFrequencyRule(); - switch ($rule) { - case PhabricatorCalendarEvent::FREQUENCY_DAILY: + $rrule = $event->newRecurrenceRule(); + + if ($rrule) { + $frequency = $rrule->getFrequency(); + } else { + $frequency = null; + } + + switch ($frequency) { + case PhutilCalendarRecurrenceRule::FREQUENCY_DAILY: if ($is_parent) { $message = pht('This event repeats every day.'); } else { $message = pht( 'This event is an instance of %s, and repeats every day.', $parent_link); } break; - case PhabricatorCalendarEvent::FREQUENCY_WEEKLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY: if ($is_parent) { $message = pht('This event repeats every week.'); } else { $message = pht( 'This event is an instance of %s, and repeats every week.', $parent_link); } break; - case PhabricatorCalendarEvent::FREQUENCY_MONTHLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY: if ($is_parent) { $message = pht('This event repeats every month.'); } else { $message = pht( 'This event is an instance of %s, and repeats every month.', $parent_link); } break; - case PhabricatorCalendarEvent::FREQUENCY_YEARLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY: if ($is_parent) { $message = pht('This event repeats every year.'); } else { $message = pht( 'This event is an instance of %s, and repeats every year.', $parent_link); } break; } $properties->addProperty(pht('Event Series'), $message); return $properties; } private function buildDescriptionView( PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer); if (strlen($event->getDescription())) { $description = new PHUIRemarkupView($viewer, $event->getDescription()); $properties->addTextContent($description); return $properties; } return null; } private function loadEvent() { $request = $this->getRequest(); $viewer = $this->getViewer(); $id = $request->getURIData('id'); $sequence = $request->getURIData('sequence'); // We're going to figure out which event you're trying to look at. Most of // the time this is simple, but you may be looking at an instance of a // recurring event which we haven't generated an object for. // If you are, we're going to generate a "stub" event so we have a real // ID and PHID to work with, since the rest of the infrastructure relies // on these identifiers existing. // Load the event identified by ID first. $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$event) { return null; } // If we aren't looking at an instance of this event, this is a completely // normal request and we can just return this event. if (!$sequence) { return $event; } // When you view "E123/999", E123 is normally the parent event. However, // you might visit a different instance first instead and then fiddle // with the URI. If the event we're looking at is a child, we are going // to act on the parent instead. if ($event->isChildEvent()) { $event = $event->getParentEvent(); } // Try to load the instance. If it already exists, we're all done and // can just return it. $instance = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withInstanceSequencePairs( array( array($event->getPHID(), $sequence), )) ->executeOne(); if ($instance) { return $instance; } if (!$viewer->isLoggedIn()) { throw new Exception( pht( 'This event instance has not been created yet. Log in to create '. 'it.')); } if (!$event->isValidSequenceIndex($viewer, $sequence)) { return null; } return $event->newStub($viewer, $sequence); } private function buildSubheaderView(PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); $host_phid = $event->getHostPHID(); $handles = $viewer->loadHandles(array($host_phid)); $handle = $handles[$host_phid]; $host = $viewer->renderHandle($host_phid); $host = phutil_tag('strong', array(), $host); $image_uri = $handles[$host_phid]->getImageURI(); $image_href = $handles[$host_phid]->getURI(); $date = $event->renderEventDate($viewer, true); $content = pht('Hosted by %s on %s.', $host, $date); return id(new PHUIHeadThingView()) ->setImage($image_uri) ->setImageHref($image_href) ->setContent($content); } private function buildRSVPActions(PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); $id = $event->getID(); $invite_status = $event->getUserInviteStatus($viewer->getPHID()); $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; $is_invite_pending = ($invite_status == $status_invited); if (!$is_invite_pending) { return array(); } $decline_button = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-times grey') ->setHref($this->getApplicationURI("/event/decline/{$id}/")) ->setWorkflow(true) ->setText(pht('Decline')); $accept_button = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-check green') ->setHref($this->getApplicationURI("/event/accept/{$id}/")) ->setWorkflow(true) ->setText(pht('Accept')); return array($decline_button, $accept_button); } } diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php index 63d33beab4..17ea7552e9 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php @@ -1,256 +1,264 @@ getViewer()); } 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/edit/'); } protected function buildCustomEditFields($object) { $viewer = $this->getViewer(); if ($this->getIsCreate()) { $invitee_phids = array($viewer->getPHID()); } else { $invitee_phids = $object->getInviteePHIDsForEdit(); } $frequency_options = array( - PhabricatorCalendarEvent::FREQUENCY_DAILY => pht('Daily'), - PhabricatorCalendarEvent::FREQUENCY_WEEKLY => pht('Weekly'), - PhabricatorCalendarEvent::FREQUENCY_MONTHLY => pht('Monthly'), - PhabricatorCalendarEvent::FREQUENCY_YEARLY => pht('Yearly'), + PhutilCalendarRecurrenceRule::FREQUENCY_DAILY => pht('Daily'), + PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY => pht('Weekly'), + PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY => pht('Monthly'), + PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY => pht('Yearly'), ); $fields = array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Name of the event.')) ->setIsRequired(true) ->setTransactionType( PhabricatorCalendarEventNameTransaction::TRANSACTIONTYPE) ->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( PhabricatorCalendarEventDescriptionTransaction::TRANSACTIONTYPE) ->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( PhabricatorCalendarEventCancelTransaction::TRANSACTIONTYPE) ->setIsConduitOnly(true) ->setConduitDescription(pht('Cancel or restore the event.')) ->setConduitTypeDescription(pht('True to cancel the event.')) ->setValue($object->getIsCancelled()), id(new PhabricatorUsersEditField()) ->setKey('hostPHID') ->setAliases(array('host')) ->setLabel(pht('Host')) ->setDescription(pht('Host of the event.')) ->setTransactionType( PhabricatorCalendarEventHostTransaction::TRANSACTIONTYPE) ->setIsConduitOnly($this->getIsCreate()) ->setConduitDescription(pht('Change the host of the event.')) ->setConduitTypeDescription(pht('New event host.')) ->setSingleValue($object->getHostPHID()), id(new PhabricatorDatasourceEditField()) ->setKey('inviteePHIDs') ->setAliases(array('invite', 'invitee', 'invitees', 'inviteePHID')) ->setLabel(pht('Invitees')) ->setDatasource(new PhabricatorMetaMTAMailableDatasource()) ->setTransactionType( PhabricatorCalendarEventInviteTransaction::TRANSACTIONTYPE) ->setDescription(pht('Users invited to the event.')) ->setConduitDescription(pht('Change invited users.')) ->setConduitTypeDescription(pht('New event invitees.')) ->setValue($invitee_phids) ->setCommentActionLabel(pht('Change Invitees')), ); if ($this->getIsCreate()) { $fields[] = id(new PhabricatorBoolEditField()) ->setKey('isRecurring') ->setLabel(pht('Recurring')) ->setOptions(pht('One-Time Event'), pht('Recurring Event')) ->setTransactionType( PhabricatorCalendarEventRecurringTransaction::TRANSACTIONTYPE) ->setDescription(pht('One time or recurring event.')) ->setConduitDescription(pht('Make the event recurring.')) ->setConduitTypeDescription(pht('Mark the event as a recurring event.')) ->setValue($object->getIsRecurring()); + + $rrule = $object->newRecurrenceRule(); + if ($rrule) { + $frequency = $rrule->getFrequency(); + } else { + $frequency = null; + } + $fields[] = id(new PhabricatorSelectEditField()) ->setKey('frequency') ->setLabel(pht('Frequency')) ->setOptions($frequency_options) ->setTransactionType( PhabricatorCalendarEventFrequencyTransaction::TRANSACTIONTYPE) ->setDescription(pht('Recurring event frequency.')) ->setConduitDescription(pht('Change the event frequency.')) ->setConduitTypeDescription(pht('New event frequency.')) - ->setValue($object->getFrequencyRule()); + ->setValue($frequency); } if ($this->getIsCreate() || $object->getIsRecurring()) { $fields[] = id(new PhabricatorEpochEditField()) ->setAllowNull(true) ->setKey('until') ->setLabel(pht('Repeat Until')) ->setTransactionType( PhabricatorCalendarEventUntilDateTransaction::TRANSACTIONTYPE) ->setDescription(pht('Last instance of the event.')) ->setConduitDescription(pht('Change when the event repeats until.')) ->setConduitTypeDescription(pht('New final event time.')) ->setValue($object->getUntilDateTimeEpoch()); } $fields[] = id(new PhabricatorBoolEditField()) ->setKey('isAllDay') ->setLabel(pht('All Day')) ->setOptions(pht('Normal Event'), pht('All Day Event')) ->setTransactionType( PhabricatorCalendarEventAllDayTransaction::TRANSACTIONTYPE) ->setDescription(pht('Marks this as an all day event.')) ->setConduitDescription(pht('Make the event an all day event.')) ->setConduitTypeDescription(pht('Mark the event as an all day event.')) ->setValue($object->getIsAllDay()); $fields[] = id(new PhabricatorEpochEditField()) ->setKey('start') ->setLabel(pht('Start')) ->setTransactionType( PhabricatorCalendarEventStartDateTransaction::TRANSACTIONTYPE) ->setDescription(pht('Start time of the event.')) ->setConduitDescription(pht('Change the start time of the event.')) ->setConduitTypeDescription(pht('New event start time.')) ->setValue($object->getStartDateTimeEpoch()); $fields[] = id(new PhabricatorEpochEditField()) ->setKey('end') ->setLabel(pht('End')) ->setTransactionType( PhabricatorCalendarEventEndDateTransaction::TRANSACTIONTYPE) ->setDescription(pht('End time of the event.')) ->setConduitDescription(pht('Change the end time of the event.')) ->setConduitTypeDescription(pht('New event end time.')) ->setValue($object->getEndDateTimeEpoch()); $fields[] = id(new PhabricatorIconSetEditField()) ->setKey('icon') ->setLabel(pht('Icon')) ->setIconSet(new PhabricatorCalendarIconSet()) ->setTransactionType( PhabricatorCalendarEventIconTransaction::TRANSACTIONTYPE) ->setDescription(pht('Event icon.')) ->setConduitDescription(pht('Change the event icon.')) ->setConduitTypeDescription(pht('New event icon.')) ->setValue($object->getIcon()); return $fields; } protected function willBuildEditForm($object, array $fields) { $all_day_field = idx($fields, 'isAllDay'); $start_field = idx($fields, 'start'); $end_field = idx($fields, 'end'); if ($all_day_field) { $is_all_day = $all_day_field->getValueForTransaction(); $control_ids = array(); if ($start_field) { $control_ids[] = $start_field->getControlID(); } if ($end_field) { $control_ids[] = $end_field->getControlID(); } Javelin::initBehavior( 'event-all-day', array( 'allDayID' => $all_day_field->getControlID(), 'controlIDs' => $control_ids, )); } else { $is_all_day = $object->getIsAllDay(); } if ($is_all_day) { if ($start_field) { $start_field->setHideTime(true); } if ($end_field) { $end_field->setHideTime(true); } } return $fields; } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 060c6fe5c5..676d85b608 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1,1062 +1,1041 @@ setViewer($actor) ->withClasses(array('PhabricatorCalendarApplication')) ->executeOne(); $view_default = PhabricatorCalendarEventDefaultViewCapability::CAPABILITY; $edit_default = PhabricatorCalendarEventDefaultEditCapability::CAPABILITY; $view_policy = $app->getPolicy($view_default); $edit_policy = $app->getPolicy($edit_default); $now = PhabricatorTime::getNow(); $default_icon = 'fa-calendar'; $datetime_start = PhutilCalendarAbsoluteDateTime::newFromEpoch( $now, $actor->getTimezoneIdentifier()); $datetime_end = $datetime_start->newRelativeDateTime('PT1H'); return id(new PhabricatorCalendarEvent()) ->setHostPHID($actor->getPHID()) ->setIsCancelled(0) ->setIsAllDay(0) ->setIsStub(0) ->setIsRecurring(0) - ->setRecurrenceFrequency( - array( - 'rule' => self::FREQUENCY_WEEKLY, - )) ->setIcon($default_icon) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()) ->attachInvitees(array()) ->setDateFrom(0) ->setDateTo(0) ->setAllDayDateFrom(0) ->setAllDayDateTo(0) ->setStartDateTime($datetime_start) ->setEndDateTime($datetime_end) ->applyViewerTimezone($actor); } private function newChild( PhabricatorUser $actor, $sequence, PhutilCalendarDateTime $start = null) { 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) ->setAllDayDateFrom(0) ->setAllDayDateTo(0) ->setDateFrom(0) ->setDateTo(0); return $child->copyFromParent($actor, $start); } protected function readField($field) { static $inherit = array( 'hostPHID' => 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 apply to // the child. 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, PhutilCalendarDateTime $start = null) { if (!$this->isChildEvent()) { throw new Exception( pht( 'Unable to copy from parent event: this is not a child event.')); } $parent = $this->getParentEvent(); $this ->setHostPHID($parent->getHostPHID()) ->setIsAllDay($parent->getIsAllDay()) ->setIcon($parent->getIcon()) ->setSpacePHID($parent->getSpacePHID()) ->setViewPolicy($parent->getViewPolicy()) ->setEditPolicy($parent->getEditPolicy()) ->setName($parent->getName()) ->setDescription($parent->getDescription()); $sequence = $this->getSequenceIndex(); if ($start) { $start_datetime = $start; } else { $start_datetime = $parent->newSequenceIndexDateTime($sequence); if (!$start_datetime) { throw new Exception( pht( 'Sequence "%s" is not valid for event!', $sequence)); } } $duration = $parent->newDuration(); $end_datetime = $start_datetime->newRelativeDateTime($duration); $this ->setStartDateTime($start_datetime) ->setEndDateTime($end_datetime); return $this; } public function isValidSequenceIndex(PhabricatorUser $viewer, $sequence) { return (bool)$this->newSequenceIndexDateTime($sequence); } public function newSequenceIndexDateTime($sequence) { $set = $this->newRecurrenceSet(); if (!$set) { return null; } $instances = $set->getEventsBetween( null, $this->newUntilDateTime(), $sequence + 1); return idx($instances, $sequence, null); } 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, PhutilCalendarDateTime $start = null) { $ghost = $this->newChild($actor, $sequence, $start); $ghost ->setIsGhostEvent(true) ->makeEphemeral(); $ghost->applyViewerTimezone($actor); return $ghost; } public function applyViewerTimezone(PhabricatorUser $viewer) { $this->viewerTimezone = $viewer->getTimezoneIdentifier(); return $this; } public function getDuration() { return ($this->getEndDateTimeEpoch() - $this->getStartDateTimeEpoch()); } public function updateUTCEpochs() { // The "intitial" epoch is the start time of the event, in UTC. $start_date = $this->newStartDateTime() ->setViewerTimezone('UTC'); $start_epoch = $start_date->getEpoch(); $this->setUTCInitialEpoch($start_epoch); // The "until" epoch is the last UTC epoch on which any instance of this // event occurs. For infinitely recurring events, it is `null`. if (!$this->getIsRecurring()) { $end_date = $this->newEndDateTime() ->setViewerTimezone('UTC'); $until_epoch = $end_date->getEpoch(); } else { $until_epoch = null; $until_date = $this->newUntilDateTime(); if ($until_date) { $until_date->setViewerTimezone('UTC'); $duration = $this->newDuration(); $until_epoch = id(new PhutilCalendarRelativeDateTime()) ->setOrigin($until_date) ->setDuration($duration) ->getEpoch(); } } $this->setUTCUntilEpoch($until_epoch); // The "instance" epoch is a property of instances of recurring events. // It's the original UTC epoch on which the instance started. Usually that // is the same as the start date, but they may be different if the instance // has been edited. // The ICS format uses this value (original start time) to identify event // instances, and must do so because it allows additional arbitrary // instances to be added (with "RDATE"). $instance_epoch = null; $instance_date = $this->newInstanceDateTime(); if ($instance_date) { $instance_epoch = $instance_date ->setViewerTimezone('UTC') ->getEpoch(); } $this->setUTCInstanceEpoch($instance_epoch); return $this; } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } $this->updateUTCEpochs(); 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 getStartDateTimeEpochForCache() { $epoch = $this->getStartDateTimeEpoch(); $window = phutil_units('15 minutes in seconds'); return ($epoch - $window); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text', 'description' => 'text', 'isCancelled' => 'bool', 'isAllDay' => 'bool', 'icon' => 'text32', 'mailKey' => 'bytes20', 'isRecurring' => 'bool', 'instanceOfEventPHID' => 'phid?', 'sequenceIndex' => 'uint32?', 'isStub' => 'bool', 'utcInitialEpoch' => 'epoch', 'utcUntilEpoch' => 'epoch?', 'utcInstanceEpoch' => 'epoch?', // TODO: DEPRECATED. 'allDayDateFrom' => 'epoch', 'allDayDateTo' => 'epoch', 'dateFrom' => 'epoch', 'dateTo' => 'epoch', 'recurrenceEndDate' => 'epoch?', ), self::CONFIG_KEY_SCHEMA => array( 'key_date' => array( 'columns' => array('dateFrom', 'dateTo'), ), 'key_instance' => array( 'columns' => array('instanceOfEventPHID', 'sequenceIndex'), 'unique' => true, ), 'key_epoch' => array( 'columns' => array('utcInitialEpoch', 'utcUntilEpoch'), ), 'key_rdate' => array( 'columns' => array('instanceOfEventPHID', 'utcInstanceEpoch'), 'unique' => true, ), ), self::CONFIG_SERIALIZATION => array( 'recurrenceFrequency' => self::SERIALIZATION_JSON, 'parameters' => 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 getFrequencyRule() { - return idx($this->recurrenceFrequency, 'rule'); - } - - public function getFrequencyUnit() { - $frequency = $this->getFrequencyRule(); - - 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->getIsRecurring() && !$this->getInstanceOfEventPHID()); } 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 renderEventDate( PhabricatorUser $viewer, $show_end) { $start = $this->newStartDateTime(); $end = $this->newEndDateTime(); if ($show_end) { $min_date = $start->newPHPDateTime(); $max_date = $end->newPHPDateTime(); $min_day = $min_date->format('Y m d'); $max_day = $max_date->format('Y m d'); $show_end_date = ($min_day != $max_day); } else { $show_end_date = false; } $min_epoch = $start->getEpoch(); $max_epoch = $end->getEpoch(); if ($this->getIsAllDay()) { if ($show_end_date) { return pht( '%s - %s, All Day', phabricator_date($min_epoch, $viewer), phabricator_date($max_epoch, $viewer)); } else { return pht( '%s, All Day', phabricator_date($min_epoch, $viewer)); } } else if ($show_end_date) { return pht( '%s - %s', phabricator_datetime($min_epoch, $viewer), phabricator_datetime($max_epoch, $viewer)); } else if ($show_end) { return pht( '%s - %s', phabricator_datetime($min_epoch, $viewer), phabricator_time($max_epoch, $viewer)); } else { return pht( '%s', phabricator_datetime($min_epoch, $viewer)); } } public function getDisplayIcon(PhabricatorUser $viewer) { if ($this->isCancelledEvent()) { return 'fa-times'; } if ($viewer->isLoggedIn()) { $status = $this->getUserInviteStatus($viewer->getPHID()); switch ($status) { case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: return 'fa-check-circle'; case PhabricatorCalendarEventInvitee::STATUS_INVITED: return 'fa-user-plus'; case PhabricatorCalendarEventInvitee::STATUS_DECLINED: return 'fa-times'; } } return $this->getIcon(); } public function getDisplayIconColor(PhabricatorUser $viewer) { if ($this->isCancelledEvent()) { return 'red'; } if ($viewer->isLoggedIn()) { $status = $this->getUserInviteStatus($viewer->getPHID()); switch ($status) { case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: return 'green'; case PhabricatorCalendarEventInvitee::STATUS_INVITED: return 'green'; case PhabricatorCalendarEventInvitee::STATUS_DECLINED: return 'grey'; } } return 'bluegrey'; } public function getDisplayIconLabel(PhabricatorUser $viewer) { if ($this->isCancelledEvent()) { return pht('Cancelled'); } if ($viewer->isLoggedIn()) { $status = $this->getUserInviteStatus($viewer->getPHID()); switch ($status) { case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: return pht('Attending'); case PhabricatorCalendarEventInvitee::STATUS_INVITED: return pht('Invited'); case PhabricatorCalendarEventInvitee::STATUS_DECLINED: return pht('Declined'); } } return null; } public function getICSFilename() { return $this->getMonogram().'.ics'; } public function newIntermediateEventNode(PhabricatorUser $viewer) { $base_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/')); $domain = $base_uri->getDomain(); $uid = $this->getPHID().'@'.$domain; $created = $this->getDateCreated(); $created = PhutilCalendarAbsoluteDateTime::newFromEpoch($created); $modified = $this->getDateModified(); $modified = PhutilCalendarAbsoluteDateTime::newFromEpoch($modified); $date_start = $this->newStartDateTime(); $date_end = $this->newEndDateTime(); if ($this->getIsAllDay()) { $date_start->setIsAllDay(true); $date_end->setIsAllDay(true); } $host_phid = $this->getHostPHID(); $invitees = $this->getInvitees(); foreach ($invitees as $key => $invitee) { if ($invitee->isUninvited()) { unset($invitees[$key]); } } $phids = array(); $phids[] = $host_phid; foreach ($invitees as $invitee) { $phids[] = $invitee->getInviteePHID(); } $handles = $viewer->loadHandles($phids); $host_handle = $handles[$host_phid]; $host_name = $host_handle->getFullName(); $host_uri = $host_handle->getURI(); $host_uri = PhabricatorEnv::getURI($host_uri); $organizer = id(new PhutilCalendarUserNode()) ->setName($host_name) ->setURI($host_uri); $attendees = array(); foreach ($invitees as $invitee) { $invitee_phid = $invitee->getInviteePHID(); $invitee_handle = $handles[$invitee_phid]; $invitee_name = $invitee_handle->getFullName(); $invitee_uri = $invitee_handle->getURI(); $invitee_uri = PhabricatorEnv::getURI($invitee_uri); switch ($invitee->getStatus()) { case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: $status = PhutilCalendarUserNode::STATUS_ACCEPTED; break; case PhabricatorCalendarEventInvitee::STATUS_DECLINED: $status = PhutilCalendarUserNode::STATUS_DECLINED; break; case PhabricatorCalendarEventInvitee::STATUS_INVITED: default: $status = PhutilCalendarUserNode::STATUS_INVITED; break; } $attendees[] = id(new PhutilCalendarUserNode()) ->setName($invitee_name) ->setURI($invitee_uri) ->setStatus($status); } $node = id(new PhutilCalendarEventNode()) ->setUID($uid) ->setName($this->getName()) ->setDescription($this->getDescription()) ->setCreatedDateTime($created) ->setModifiedDateTime($modified) ->setStartDateTime($date_start) ->setEndDateTime($date_end) ->setOrganizer($organizer) ->setAttendees($attendees); return $node; } public function newStartDateTime() { $datetime = $this->getParameter('startDateTime'); if ($datetime) { return $this->newDateTimeFromDictionary($datetime); } $epoch = $this->getDateFrom(); return $this->newDateTimeFromEpoch($epoch); } public function getStartDateTimeEpoch() { return $this->newStartDateTime()->getEpoch(); } public function newEndDateTime() { $datetime = $this->getParameter('endDateTime'); if ($datetime) { return $this->newDateTimeFromDictionary($datetime); } $epoch = $this->getDateTo(); return $this->newDateTimeFromEpoch($epoch); } public function getEndDateTimeEpoch() { return $this->newEndDateTime()->getEpoch(); } public function newUntilDateTime() { $datetime = $this->getParameter('untilDateTime'); if ($datetime) { return $this->newDateTimeFromDictionary($datetime); } $epoch = $this->getRecurrenceEndDate(); if (!$epoch) { return null; } return $this->newDateTimeFromEpoch($epoch); } public function getUntilDateTimeEpoch() { $datetime = $this->newUntilDateTime(); if (!$datetime) { return null; } return $datetime->getEpoch(); } public function newDuration() { return id(new PhutilCalendarDuration()) ->setSeconds($this->getDuration()); } public function newInstanceDateTime() { if (!$this->getIsRecurring()) { return null; } $index = $this->getSequenceIndex(); if (!$index) { return null; } return $this->newSequenceIndexDateTime($index); } private function newDateTimeFromEpoch($epoch) { $datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($epoch); if ($this->getIsAllDay()) { $datetime->setIsAllDay(true); } return $this->newDateTimeFromDateTime($datetime); } private function newDateTimeFromDictionary(array $dict) { $datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($dict); return $this->newDateTimeFromDateTime($datetime); } private function newDateTimeFromDateTime(PhutilCalendarDateTime $datetime) { $viewer_timezone = $this->viewerTimezone; if ($viewer_timezone) { $datetime->setViewerTimezone($viewer_timezone); } return $datetime; } public function getParameter($key, $default = null) { return idx($this->parameters, $key, $default); } public function setParameter($key, $value) { $this->parameters[$key] = $value; return $this; } public function setStartDateTime(PhutilCalendarDateTime $datetime) { return $this->setParameter( 'startDateTime', $datetime->newAbsoluteDateTime()->toDictionary()); } public function setEndDateTime(PhutilCalendarDateTime $datetime) { return $this->setParameter( 'endDateTime', $datetime->newAbsoluteDateTime()->toDictionary()); } public function setUntilDateTime(PhutilCalendarDateTime $datetime) { return $this->setParameter( 'untilDateTime', $datetime->newAbsoluteDateTime()->toDictionary()); } + public function setRecurrenceRule(PhutilCalendarRecurrenceRule $rrule) { + return $this->setParameter( + 'recurrenceRule', + $rrule->toDictionary()); + } public function newRecurrenceRule() { if ($this->isChildEvent()) { return $this->getParentEvent()->newRecurrenceRule(); } - // TODO: This is a little fragile since it relies on the convenient - // definition of FREQUENCY constants here and in RecurrenceRule, but - // should be gone soon. - $map = array( - 'FREQ' => phutil_utf8_strtoupper($this->getFrequencyRule()), - ); + if (!$this->getIsRecurring()) { + return null; + } + + $dict = $this->getParameter('recurrenceRule'); + if (!$dict) { + return null; + } - $rrule = PhutilCalendarRecurrenceRule::newFromDictionary($map); + $rrule = PhutilCalendarRecurrenceRule::newFromDictionary($dict); $start = $this->newStartDateTime(); $rrule->setStartDateTime($start); return $rrule; } public function newRecurrenceSet() { if ($this->isChildEvent()) { return $this->getParentEvent()->newRecurrenceSet(); } $set = new PhutilCalendarRecurrenceSet(); $rrule = $this->newRecurrenceRule(); + if (!$rrule) { + return null; + } + $set->addSource($rrule); return $set; } /* -( 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 host of an event can always view and edit it. $user_phid = $this->getHostPHID(); 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 host of an event can always view and edit it. Users who are '. 'invited to an event can always view it.'); } /* -( 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->getHostPHID()); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array($this->getHostPHID()); } /* -( 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(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the event.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('description') ->setType('string') ->setDescription(pht('The event description.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'description' => $this->getDescription(), ); } public function getConduitSearchAttachments() { return array(); } } diff --git a/src/applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php index 9690a97dce..2da4f1da62 100644 --- a/src/applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php +++ b/src/applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php @@ -1,75 +1,115 @@ getFrequencyRule(); + $rrule = $object->newRecurrenceRule(); + + if (!$rrule) { + return null; + } + + return $rrule->getFrequency(); } public function applyInternalEffects($object, $value) { - $object->setRecurrenceFrequency( - array( - 'rule' => $value, - )); + $rrule = id(new PhutilCalendarRecurrenceRule()) + ->setFrequency($value); + + $dict = $rrule->toDictionary(); + $object->setRecurrenceRule($dict); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $valid = array( + PhutilCalendarRecurrenceRule::FREQUENCY_DAILY, + PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY, + PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY, + PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY, + ); + $valid = array_fuse($valid); + + foreach ($xactions as $xaction) { + $value = $xaction->getNewValue(); + + if (!isset($valid[$value])) { + $errors[] = $this->newInvalidError( + pht( + 'Event frequency "%s" is not valid. Valid frequences are: %s.', + $value, + implode(', ', $valid)), + $xaction); + } + } + + return $errors; } public function getTitle() { - $frequency = $this->getFrequencyRule($this->getNewValue()); + $frequency = $this->getFrequency($this->getNewValue()); switch ($frequency) { - case PhabricatorCalendarEvent::FREQUENCY_DAILY: + case PhutilCalendarRecurrenceRule::FREQUENCY_DAILY: return pht( '%s set this event to repeat daily.', $this->renderAuthor()); - case PhabricatorCalendarEvent::FREQUENCY_WEEKLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY: return pht( '%s set this event to repeat weekly.', $this->renderAuthor()); - case PhabricatorCalendarEvent::FREQUENCY_MONTHLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY: return pht( '%s set this event to repeat monthly.', $this->renderAuthor()); - case PhabricatorCalendarEvent::FREQUENCY_YEARLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY: return pht( '%s set this event to repeat yearly.', $this->renderAuthor()); } } public function getTitleForFeed() { - $frequency = $this->getFrequencyRule($this->getNewValue()); + $frequency = $this->getFrequency($this->getNewValue()); switch ($frequency) { - case PhabricatorCalendarEvent::FREQUENCY_DAILY: + case PhutilCalendarRecurrenceRule::FREQUENCY_DAILY: return pht( '%s set %s to repeat daily.', $this->renderAuthor(), $this->renderObject()); - case PhabricatorCalendarEvent::FREQUENCY_WEEKLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY: return pht( '%s set %s to repeat weekly.', $this->renderAuthor(), $this->renderObject()); - case PhabricatorCalendarEvent::FREQUENCY_MONTHLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY: return pht( '%s set %s to repeat monthly.', $this->renderAuthor(), $this->renderObject()); - case PhabricatorCalendarEvent::FREQUENCY_YEARLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY: return pht( '%s set %s to repeat yearly.', $this->renderAuthor(), $this->renderObject()); } } - private function getFrequencyRule($value) { + private function getFrequency($value) { + // NOTE: This is normalizing three generations of these transactions + // to use RRULE constants. It would be vaguely nice to migrate them + // for consistency. + if (is_array($value)) { $value = idx($value, 'rule'); } else { - return $value; + $value = $value; } + + return phutil_utf8_strtoupper($value); } }