@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Use transactions when importing events in Calendar, and update existing events

Summary:
Ref T10747.

- Apply what changes we can with transactions, so you can see how an event has changed and import actions are more explicit.
- I'll hide these from email/feed soon: I want them to appear on the event, but not generate notifications, since that could be especially annoying for automated events.
- When importing, try to update existing events if we can.

Test Plan:
Imported a ".ics" file several times with minor changes, saw them reflected in the UI with transactions.

{F1870027}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10747

Differential Revision: https://secure.phabricator.com/D16701

+224 -71
+198 -11
src/applications/calendar/import/PhabricatorCalendarImportEngine.php
··· 36 36 37 37 $event_type = PhutilCalendarEventNode::NODETYPE; 38 38 39 - $events = array(); 39 + $nodes = array(); 40 40 foreach ($root->getChildren() as $document) { 41 41 foreach ($document->getChildren() as $node) { 42 42 if ($node->getNodeType() != $event_type) { ··· 44 44 continue; 45 45 } 46 46 47 - $event = PhabricatorCalendarEvent::newFromDocumentNode($viewer, $node); 47 + $nodes[] = $node; 48 + } 49 + } 50 + 51 + $node_map = array(); 52 + $parent_uids = array(); 53 + foreach ($nodes as $node) { 54 + $full_uid = $this->getFullNodeUID($node); 55 + if (isset($node_map[$full_uid])) { 56 + // TODO: Warn that we got a duplicate. 57 + continue; 58 + } 59 + $node_map[$full_uid] = $node; 60 + } 61 + 62 + if ($node_map) { 63 + $events = id(new PhabricatorCalendarEventQuery()) 64 + ->setViewer($viewer) 65 + ->withImportAuthorPHIDs(array($viewer->getPHID())) 66 + ->withImportUIDs(array_keys($node_map)) 67 + ->execute(); 68 + $events = mpull($events, null, 'getImportUID'); 69 + } else { 70 + $events = null; 71 + } 72 + 73 + $xactions = array(); 74 + $update_map = array(); 75 + foreach ($node_map as $full_uid => $node) { 76 + $event = idx($events, $full_uid); 77 + if (!$event) { 78 + $event = PhabricatorCalendarEvent::initializeNewCalendarEvent($viewer); 79 + } 80 + 81 + $event 82 + ->setImportAuthorPHID($viewer->getPHID()) 83 + ->setImportSourcePHID($import->getPHID()) 84 + ->setImportUID($full_uid) 85 + ->attachImportSource($import); 86 + 87 + $this->updateEventFromNode($viewer, $event, $node); 88 + $xactions[$full_uid] = $this->newUpdateTransactions($event, $node); 89 + $update_map[$full_uid] = $event; 90 + } 91 + 92 + // Reorder events so we create parents first. This allows us to populate 93 + // "instanceOfEventPHID" correctly. 94 + $insert_order = array(); 95 + foreach ($update_map as $full_uid => $event) { 96 + $parent_uid = $this->getParentNodeUID($node_map[$full_uid]); 97 + if ($parent_uid === null) { 98 + $insert_order[$full_uid] = $full_uid; 99 + continue; 100 + } 48 101 49 - $event 50 - ->setImportAuthorPHID($viewer->getPHID()) 51 - ->setImportSourcePHID($import->getPHID()) 52 - ->attachImportSource($import); 102 + if (empty($update_map[$parent_uid])) { 103 + // The parent was not present in this import, which means it either 104 + // does not exist or we're going to delete it anyway. We just drop 105 + // this node. 53 106 54 - $events[] = $event; 107 + // TODO: Warn that we got rid of an event with no parent. 108 + 109 + continue; 55 110 } 111 + 112 + // Otherwise, we're going to insert the parent first, then insert 113 + // the child. 114 + $insert_order[$parent_uid] = $parent_uid; 115 + $insert_order[$full_uid] = $full_uid; 56 116 } 57 117 58 - // TODO: Use transactions. 59 - // TODO: Update existing events instead of fataling. 60 - foreach ($events as $event) { 61 - $event->save(); 118 + // TODO: Define per-engine content sources so this can say "via Upload" or 119 + // whatever. 120 + $content_source = PhabricatorContentSource::newForSource( 121 + PhabricatorWebContentSource::SOURCECONST); 122 + 123 + $update_map = array_select_keys($update_map, $insert_order); 124 + foreach ($update_map as $full_uid => $event) { 125 + $parent_uid = $this->getParentNodeUID($node_map[$full_uid]); 126 + if ($parent_uid) { 127 + $parent_phid = $update_map[$full_uid]->getPHID(); 128 + } else { 129 + $parent_phid = null; 130 + } 131 + 132 + $event->setInstanceOfEventPHID($parent_phid); 133 + 134 + $event_xactions = $xactions[$full_uid]; 135 + 136 + $editor = id(new PhabricatorCalendarEventEditor()) 137 + ->setActor($viewer) 138 + ->setActingAsPHID($import->getPHID()) 139 + ->setContentSource($content_source) 140 + ->setContinueOnNoEffect(true) 141 + ->setContinueOnMissingFields(true); 142 + 143 + $editor->applyTransactions($event, $event_xactions); 144 + } 145 + 146 + // TODO: When the source is a subscription-based ICS file or some other 147 + // similar source, we should load all events from the source here and 148 + // destroy the ones we didn't update. These are events that have been 149 + // deleted. 150 + } 151 + 152 + private function getFullNodeUID(PhutilCalendarEventNode $node) { 153 + $uid = $node->getUID(); 154 + $instance_epoch = $this->getNodeInstanceEpoch($node); 155 + $full_uid = $uid.'/'.$instance_epoch; 156 + 157 + return $full_uid; 158 + } 159 + 160 + private function getParentNodeUID(PhutilCalendarEventNode $node) { 161 + $recurrence_id = $node->getRecurrenceID(); 162 + 163 + if (!strlen($recurrence_id)) { 164 + return null; 62 165 } 63 166 167 + return $node->getUID().'/'; 64 168 } 65 169 170 + private function getNodeInstanceEpoch(PhutilCalendarEventNode $node) { 171 + $instance_iso = $node->getRecurrenceID(); 172 + if (strlen($instance_iso)) { 173 + $instance_datetime = PhutilCalendarAbsoluteDateTime::newFromISO8601( 174 + $instance_iso); 175 + $instance_epoch = $instance_datetime->getEpoch(); 176 + } else { 177 + $instance_epoch = null; 178 + } 66 179 180 + return $instance_epoch; 181 + } 182 + 183 + private function newUpdateTransactions( 184 + PhabricatorCalendarEvent $event, 185 + PhutilCalendarEventNode $node) { 186 + 187 + $xactions = array(); 188 + $uid = $node->getUID(); 189 + 190 + $name = $node->getName(); 191 + if (!strlen($name)) { 192 + if (strlen($uid)) { 193 + $name = pht('Unnamed Event "%s"', $uid); 194 + } else { 195 + $name = pht('Unnamed Imported Event'); 196 + } 197 + } 198 + $xactions[] = id(new PhabricatorCalendarEventTransaction()) 199 + ->setTransactionType( 200 + PhabricatorCalendarEventNameTransaction::TRANSACTIONTYPE) 201 + ->setNewValue($name); 202 + 203 + $description = $node->getDescription(); 204 + $xactions[] = id(new PhabricatorCalendarEventTransaction()) 205 + ->setTransactionType( 206 + PhabricatorCalendarEventDescriptionTransaction::TRANSACTIONTYPE) 207 + ->setNewValue((string)$description); 208 + 209 + $is_recurring = (bool)$node->getRecurrenceRule(); 210 + $xactions[] = id(new PhabricatorCalendarEventTransaction()) 211 + ->setTransactionType( 212 + PhabricatorCalendarEventRecurringTransaction::TRANSACTIONTYPE) 213 + ->setNewValue($is_recurring); 214 + 215 + return $xactions; 216 + } 217 + 218 + private function updateEventFromNode( 219 + PhabricatorUser $actor, 220 + PhabricatorCalendarEvent $event, 221 + PhutilCalendarEventNode $node) { 222 + 223 + $instance_epoch = $this->getNodeInstanceEpoch($node); 224 + $event->setUTCInstanceEpoch($instance_epoch); 225 + 226 + $timezone = $actor->getTimezoneIdentifier(); 227 + 228 + // TODO: These should be transactional, but the transaction only accepts 229 + // epoch timestamps right now. 230 + $start_datetime = $node->getStartDateTime() 231 + ->setViewerTimezone($timezone); 232 + $end_datetime = $node->getEndDateTime() 233 + ->setViewerTimezone($timezone); 234 + 235 + $event 236 + ->setStartDateTime($start_datetime) 237 + ->setEndDateTime($end_datetime); 238 + 239 + // TODO: This should be transactional, but the transaction only accepts 240 + // simple frequency rules right now. 241 + $rrule = $node->getRecurrenceRule(); 242 + if ($rrule) { 243 + $event->setRecurrenceRule($rrule); 244 + 245 + $until_datetime = $rrule->getUntil() 246 + ->setViewerTimezone($timezone); 247 + if ($until_datetime) { 248 + $event->setUntilDateTime($until_datetime); 249 + } 250 + } 251 + 252 + return $event; 253 + } 67 254 68 255 }
+26
src/applications/calendar/query/PhabricatorCalendarEventQuery.php
··· 15 15 private $isStub; 16 16 private $parentEventPHIDs; 17 17 private $importSourcePHIDs; 18 + private $importAuthorPHIDs; 19 + private $importUIDs; 18 20 19 21 private $generateGhosts = false; 20 22 ··· 80 82 81 83 public function withImportSourcePHIDs(array $import_phids) { 82 84 $this->importSourcePHIDs = $import_phids; 85 + return $this; 86 + } 87 + 88 + public function withImportAuthorPHIDs(array $author_phids) { 89 + $this->importAuthorPHIDs = $author_phids; 90 + return $this; 91 + } 92 + 93 + public function withImportUIDs(array $uids) { 94 + $this->importUIDs = $uids; 83 95 return $this; 84 96 } 85 97 ··· 422 434 $conn, 423 435 'event.importSourcePHID IN (%Ls)', 424 436 $this->importSourcePHIDs); 437 + } 438 + 439 + if ($this->importAuthorPHIDs !== null) { 440 + $where[] = qsprintf( 441 + $conn, 442 + 'event.importAuthorPHID IN (%Ls)', 443 + $this->importAuthorPHIDs); 444 + } 445 + 446 + if ($this->importUIDs !== null) { 447 + $where[] = qsprintf( 448 + $conn, 449 + 'event.importUID IN (%Ls)', 450 + $this->importUIDs); 425 451 } 426 452 427 453 return $where;
-60
src/applications/calendar/storage/PhabricatorCalendarEvent.php
··· 102 102 ->applyViewerTimezone($actor); 103 103 } 104 104 105 - public static function newFromDocumentNode( 106 - PhabricatorUser $actor, 107 - PhutilCalendarEventNode $node) { 108 - $timezone = $actor->getTimezoneIdentifier(); 109 - 110 - $uid = $node->getUID(); 111 - 112 - $name = $node->getName(); 113 - if (!strlen($name)) { 114 - if (strlen($uid)) { 115 - $name = pht('Unnamed Event "%s"', $node->getUID()); 116 - } else { 117 - $name = pht('Unnamed Imported Event'); 118 - } 119 - } 120 - 121 - $description = $node->getDescription(); 122 - 123 - $instance_iso = $node->getRecurrenceID(); 124 - if (strlen($instance_iso)) { 125 - $instance_datetime = PhutilCalendarAbsoluteDateTime::newFromISO8601( 126 - $instance_iso); 127 - $instance_epoch = $instance_datetime->getEpoch(); 128 - } else { 129 - $instance_epoch = null; 130 - } 131 - $full_uid = $uid.'/'.$instance_epoch; 132 - 133 - $start_datetime = $node->getStartDateTime() 134 - ->setViewerTimezone($timezone); 135 - $end_datetime = $node->getEndDateTime() 136 - ->setViewerTimezone($timezone); 137 - 138 - $rrule = $node->getRecurrenceRule(); 139 - 140 - $event = self::initializeNewCalendarEvent($actor) 141 - ->setName($name) 142 - ->setStartDateTime($start_datetime) 143 - ->setEndDateTime($end_datetime) 144 - ->setImportUID($full_uid) 145 - ->setUTCInstanceEpoch($instance_epoch); 146 - 147 - if (strlen($description)) { 148 - $event->setDescription($description); 149 - } 150 - 151 - if ($rrule) { 152 - $event->setRecurrenceRule($rrule); 153 - $event->setIsRecurring(1); 154 - 155 - $until_datetime = $rrule->getUntil() 156 - ->setViewerTimezone($timezone); 157 - if ($until_datetime) { 158 - $event->setUntilDateTime($until_datetime); 159 - } 160 - } 161 - 162 - return $event; 163 - } 164 - 165 105 private function newChild( 166 106 PhabricatorUser $actor, 167 107 $sequence,