@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.

DRAFT Add db columns for recurring events

Summary: Ref T2896, DRAFT Add db columns for recurring events

Test Plan: Open event, confirm it still works.

Reviewers: chad, #blessed_reviewers, epriestley

Reviewed By: #blessed_reviewers, epriestley

Subscribers: btrahan, Korvin, epriestley

Maniphest Tasks: T2896

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

+354 -30
+5 -3
resources/celerity/map.php
··· 7 7 */ 8 8 return array( 9 9 'names' => array( 10 - 'core.pkg.css' => '68d4f4fb', 10 + 'core.pkg.css' => 'e2460e8f', 11 11 'core.pkg.js' => '3bbe23c6', 12 12 'darkconsole.pkg.js' => 'e7393ebb', 13 13 'differential.pkg.css' => '30602b8c', ··· 135 135 'rsrc/css/phui/phui-feed-story.css' => 'c9f3a0b5', 136 136 'rsrc/css/phui/phui-fontkit.css' => 'dd8ddf27', 137 137 'rsrc/css/phui/phui-form-view.css' => '808329f2', 138 - 'rsrc/css/phui/phui-form.css' => 'f535f938', 138 + 'rsrc/css/phui/phui-form.css' => '25876baf', 139 139 'rsrc/css/phui/phui-header-view.css' => '75aaf372', 140 140 'rsrc/css/phui/phui-icon.css' => 'bc766998', 141 141 'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8', ··· 333 333 'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18', 334 334 'rsrc/js/application/calendar/behavior-day-view.js' => '5c46cff2', 335 335 'rsrc/js/application/calendar/behavior-event-all-day.js' => '38dcf3c8', 336 + 'rsrc/js/application/calendar/behavior-recurring-edit.js' => '85c73ceb', 336 337 'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408', 337 338 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '01774ab2', 338 339 'rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js' => 'cf86d16a', ··· 629 630 'javelin-behavior-project-boards' => 'ba4fa35c', 630 631 'javelin-behavior-project-create' => '065227cc', 631 632 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 633 + 'javelin-behavior-recurring-edit' => '85c73ceb', 632 634 'javelin-behavior-refresh-csrf' => '7814b593', 633 635 'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf', 634 636 'javelin-behavior-releeph-request-state-change' => 'a0b57eb8', ··· 775 777 'phui-feed-story-css' => 'c9f3a0b5', 776 778 'phui-font-icon-base-css' => '3dad2ae3', 777 779 'phui-fontkit-css' => 'dd8ddf27', 778 - 'phui-form-css' => 'f535f938', 780 + 'phui-form-css' => '25876baf', 779 781 'phui-form-view-css' => '808329f2', 780 782 'phui-header-view-css' => '75aaf372', 781 783 'phui-icon-view-css' => 'bc766998',
+17
resources/sql/autopatches/20150527.calendar.recurringevents.sql
··· 1 + ALTER TABLE {$NAMESPACE}_calendar.calendar_event 2 + ADD isRecurring BOOL NOT NULL; 3 + 4 + ALTER TABLE {$NAMESPACE}_calendar.calendar_event 5 + ADD recurrenceFrequency LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL; 6 + 7 + ALTER TABLE {$NAMESPACE}_calendar.calendar_event 8 + ADD recurrenceEndDate INT UNSIGNED; 9 + 10 + ALTER TABLE {$NAMESPACE}_calendar.calendar_event 11 + ADD instanceOfEventPHID varbinary(64); 12 + 13 + ALTER TABLE {$NAMESPACE}_calendar.calendar_event 14 + ADD sequenceIndex INT UNSIGNED; 15 + 16 + UPDATE {$NAMESPACE}_calendar.calendar_event 17 + SET recurrenceFrequency = '[]' WHERE recurrenceFrequency = '';
+2 -1
src/applications/calendar/application/PhabricatorCalendarApplication.php
··· 40 40 41 41 public function getRoutes() { 42 42 return array( 43 - '/E(?P<id>[1-9]\d*)' => 'PhabricatorCalendarEventViewController', 43 + '/E(?P<id>[1-9]\d*)(?:/(?P<sequence>\d+))?' 44 + => 'PhabricatorCalendarEventViewController', 44 45 '/calendar/' => array( 45 46 '(?:query/(?P<queryKey>[^/]+)/(?:(?P<year>\d+)/'. 46 47 '(?P<month>\d+)/)?(?:(?P<day>\d+)/)?)?'
+51 -8
src/applications/calendar/controller/PhabricatorCalendarEventEditController.php
··· 21 21 $error_end_date = true; 22 22 $validation_exception = null; 23 23 24 + $is_recurring_id = celerity_generate_unique_node_id(); 25 + $frequency_id = celerity_generate_unique_node_id(); 24 26 $all_day_id = celerity_generate_unique_node_id(); 25 27 $start_date_id = celerity_generate_unique_node_id(); 26 - $end_date_id = null; 28 + $end_date_id = celerity_generate_unique_node_id(); 27 29 28 30 $next_workflow = $request->getStr('next'); 29 31 $uri_query = $request->getStr('query'); ··· 70 72 $subscribers = array(); 71 73 $invitees = array($user_phid); 72 74 $cancel_uri = $this->getApplicationURI(); 73 - $end_date_id = celerity_generate_unique_node_id(); 74 75 } else { 75 76 $event = id(new PhabricatorCalendarEventQuery()) 76 77 ->setViewer($viewer) ··· 113 114 $name = $event->getName(); 114 115 $description = $event->getDescription(); 115 116 $is_all_day = $event->getIsAllDay(); 117 + $is_recurring = $event->getIsRecurring(); 118 + $frequency = idx($event->getRecurrenceFrequency(), 'rule'); 116 119 $icon = $event->getIcon(); 117 120 118 121 $current_policies = id(new PhabricatorPolicyQuery()) ··· 134 137 $subscribers = $request->getArr('subscribers'); 135 138 $edit_policy = $request->getStr('editPolicy'); 136 139 $view_policy = $request->getStr('viewPolicy'); 140 + $is_recurring = $request->getStr('isRecurring') ? 1 : 0; 141 + $frequency = $request->getStr('frequency'); 137 142 $is_all_day = $request->getStr('isAllDay'); 138 143 $icon = $request->getStr('icon'); 139 144 ··· 154 159 155 160 $xactions[] = id(new PhabricatorCalendarEventTransaction()) 156 161 ->setTransactionType( 162 + PhabricatorCalendarEventTransaction::TYPE_RECURRING) 163 + ->setNewValue($is_recurring); 164 + 165 + $xactions[] = id(new PhabricatorCalendarEventTransaction()) 166 + ->setTransactionType( 167 + PhabricatorCalendarEventTransaction::TYPE_FREQUENCY) 168 + ->setNewValue(array('rule' => $frequency)); 169 + 170 + $xactions[] = id(new PhabricatorCalendarEventTransaction()) 171 + ->setTransactionType( 157 172 PhabricatorCalendarEventTransaction::TYPE_ALL_DAY) 158 173 ->setNewValue($is_all_day); 159 174 ··· 233 248 } 234 249 } 235 250 251 + $name = id(new AphrontFormTextControl()) 252 + ->setLabel(pht('Name')) 253 + ->setName('name') 254 + ->setValue($name) 255 + ->setError($error_name); 256 + 257 + Javelin::initBehavior('recurring-edit', array( 258 + 'isRecurring' => $is_recurring_id, 259 + 'frequency' => $frequency_id, 260 + )); 261 + 262 + $is_recurring_checkbox = id(new AphrontFormCheckboxControl()) 263 + ->addCheckbox( 264 + 'isRecurring', 265 + 1, 266 + pht('Recurring Event'), 267 + $is_recurring, 268 + $is_recurring_id); 269 + 270 + $recurrence_frequency_select = id(new AphrontFormSelectControl()) 271 + ->setName('frequency') 272 + ->setOptions(array( 273 + 'daily' => pht('Daily'), 274 + 'weekly' => pht('Weekly'), 275 + 'monthly' => pht('Monthly'), 276 + 'yearly' => pht('Yearly'), 277 + )) 278 + ->setValue($frequency) 279 + ->setLabel(pht('Recurring Event Frequency')) 280 + ->setID($frequency_id) 281 + ->setDisabled(!$is_recurring); 282 + 236 283 Javelin::initBehavior('event-all-day', array( 237 284 'allDayID' => $all_day_id, 238 285 'startDateID' => $start_date_id, 239 286 'endDateID' => $end_date_id, 240 287 )); 241 - 242 - $name = id(new AphrontFormTextControl()) 243 - ->setLabel(pht('Name')) 244 - ->setName('name') 245 - ->setValue($name) 246 - ->setError($error_name); 247 288 248 289 $all_day_checkbox = id(new AphrontFormCheckboxControl()) 249 290 ->addCheckbox( ··· 323 364 ->addHiddenInput('query', $uri_query) 324 365 ->setUser($viewer) 325 366 ->appendChild($name) 367 + ->appendChild($is_recurring_checkbox) 368 + ->appendChild($recurrence_frequency_select) 326 369 ->appendChild($all_day_checkbox) 327 370 ->appendChild($start_control) 328 371 ->appendChild($end_control)
+23 -7
src/applications/calendar/controller/PhabricatorCalendarEventViewController.php
··· 17 17 $request = $this->getRequest(); 18 18 $viewer = $request->getUser(); 19 19 20 + $sequence = $request->getURIData('sequence'); 21 + 20 22 $event = id(new PhabricatorCalendarEventQuery()) 21 23 ->setViewer($viewer) 22 24 ->withIDs(array($this->id)) 23 25 ->executeOne(); 24 26 if (!$event) { 27 + return new Aphront404Response(); 28 + } 29 + 30 + if ($sequence && $event->getIsRecurring()) { 31 + $event = $event->generateNthGhost($sequence, $viewer); 32 + } else if ($sequence) { 25 33 return new Aphront404Response(); 26 34 } 27 35 ··· 127 135 $event, 128 136 PhabricatorPolicyCapability::CAN_EDIT); 129 137 130 - $actions->addAction( 131 - id(new PhabricatorActionView()) 132 - ->setName(pht('Edit Event')) 133 - ->setIcon('fa-pencil') 134 - ->setHref($this->getApplicationURI("event/edit/{$id}/")) 135 - ->setDisabled(!$can_edit) 136 - ->setWorkflow(!$can_edit)); 138 + if (!$event->getIsGhostEvent()) { 139 + $actions->addAction( 140 + id(new PhabricatorActionView()) 141 + ->setName(pht('Edit Event')) 142 + ->setIcon('fa-pencil') 143 + ->setHref($this->getApplicationURI("event/edit/{$id}/")) 144 + ->setDisabled(!$can_edit) 145 + ->setWorkflow(!$can_edit)); 146 + } 137 147 138 148 if ($is_attending) { 139 149 $actions->addAction( ··· 203 213 $properties->addProperty( 204 214 pht('Ends'), 205 215 phabricator_datetime($event->getDateTo(), $viewer)); 216 + } 217 + 218 + if ($event->getIsRecurring()) { 219 + $properties->addProperty( 220 + pht('Recurs'), 221 + idx($event->getRecurrenceFrequency(), 'rule')); 206 222 } 207 223 208 224 $properties->addProperty(
+41
src/applications/calendar/editor/PhabricatorCalendarEventEditor.php
··· 23 23 $types[] = PhabricatorCalendarEventTransaction::TYPE_ALL_DAY; 24 24 $types[] = PhabricatorCalendarEventTransaction::TYPE_ICON; 25 25 26 + $types[] = PhabricatorCalendarEventTransaction::TYPE_RECURRING; 27 + $types[] = PhabricatorCalendarEventTransaction::TYPE_FREQUENCY; 28 + $types[] = PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE; 29 + $types[] = PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT; 30 + $types[] = PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX; 31 + 26 32 $types[] = PhabricatorTransactions::TYPE_COMMENT; 27 33 $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; 28 34 $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; ··· 34 40 PhabricatorLiskDAO $object, 35 41 PhabricatorApplicationTransaction $xaction) { 36 42 switch ($xaction->getTransactionType()) { 43 + case PhabricatorCalendarEventTransaction::TYPE_RECURRING: 44 + return $object->getIsRecurring(); 45 + case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: 46 + return $object->getRecurrenceFrequency(); 47 + case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: 48 + return $object->getRecurrenceEndDate(); 49 + case PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT: 50 + return $object->getInstanceOfEventPHID(); 51 + case PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX: 52 + return $object->getSequenceIndex(); 37 53 case PhabricatorCalendarEventTransaction::TYPE_NAME: 38 54 return $object->getName(); 39 55 case PhabricatorCalendarEventTransaction::TYPE_START_DATE: ··· 72 88 PhabricatorLiskDAO $object, 73 89 PhabricatorApplicationTransaction $xaction) { 74 90 switch ($xaction->getTransactionType()) { 91 + case PhabricatorCalendarEventTransaction::TYPE_RECURRING: 92 + case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: 93 + case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: 94 + case PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT: 95 + case PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX: 75 96 case PhabricatorCalendarEventTransaction::TYPE_NAME: 76 97 case PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION: 77 98 case PhabricatorCalendarEventTransaction::TYPE_CANCEL: ··· 93 114 PhabricatorApplicationTransaction $xaction) { 94 115 95 116 switch ($xaction->getTransactionType()) { 117 + case PhabricatorCalendarEventTransaction::TYPE_RECURRING: 118 + return $object->setIsRecurring($xaction->getNewValue()); 119 + case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: 120 + return $object->setRecurrenceFrequency($xaction->getNewValue()); 121 + case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: 122 + return $object->setRecurrenceEndDate($xaction->getNewValue()); 123 + case PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT: 124 + return $object->setInstanceOfEventPHID($xaction->getNewValue()); 125 + case PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX: 126 + return $object->setSequenceIndex($xaction->getNewValue()); 96 127 case PhabricatorCalendarEventTransaction::TYPE_NAME: 97 128 $object->setName($xaction->getNewValue()); 98 129 return; ··· 126 157 PhabricatorApplicationTransaction $xaction) { 127 158 128 159 switch ($xaction->getTransactionType()) { 160 + case PhabricatorCalendarEventTransaction::TYPE_RECURRING: 161 + case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: 162 + case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: 163 + case PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT: 164 + case PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX: 129 165 case PhabricatorCalendarEventTransaction::TYPE_NAME: 130 166 case PhabricatorCalendarEventTransaction::TYPE_START_DATE: 131 167 case PhabricatorCalendarEventTransaction::TYPE_END_DATE: ··· 181 217 switch ($xaction->getTransactionType()) { 182 218 case PhabricatorCalendarEventTransaction::TYPE_ICON: 183 219 break; 220 + case PhabricatorCalendarEventTransaction::TYPE_RECURRING: 221 + case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: 222 + case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: 223 + case PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT: 224 + case PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX: 184 225 case PhabricatorCalendarEventTransaction::TYPE_START_DATE: 185 226 case PhabricatorCalendarEventTransaction::TYPE_END_DATE: 186 227 case PhabricatorCalendarEventTransaction::TYPE_CANCEL:
+66 -1
src/applications/calendar/query/PhabricatorCalendarEventQuery.php
··· 11 11 private $creatorPHIDs; 12 12 private $isCancelled; 13 13 14 + private $generateGhosts = false; 15 + 16 + public function setGenerateGhosts($generate_ghosts) { 17 + $this->generateGhosts = $generate_ghosts; 18 + return $this; 19 + } 20 + 14 21 public function withIDs(array $ids) { 15 22 $this->ids = $ids; 16 23 return $this; ··· 69 76 protected function loadPage() { 70 77 $table = new PhabricatorCalendarEvent(); 71 78 $conn_r = $table->establishConnection('r'); 79 + $viewer = $this->getViewer(); 72 80 73 81 $data = queryfx_all( 74 82 $conn_r, ··· 86 94 $event->applyViewerTimezone($this->getViewer()); 87 95 } 88 96 97 + if (!$this->generateGhosts) { 98 + return $events; 99 + } 100 + 101 + foreach ($events as $event) { 102 + $sequence_start = 0; 103 + $instance_count = null; 104 + 105 + if ($event->getIsRecurring()) { 106 + $frequency = $event->getFrequencyUnit(); 107 + $modify_key = '+1 '.$frequency; 108 + 109 + if ($this->rangeBegin && $this->rangeBegin > $event->getDateFrom()) { 110 + $max_date = $this->rangeBegin; 111 + $date = $event->getDateFrom(); 112 + $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); 113 + 114 + while ($date < $max_date) { 115 + // TODO: optimize this to not loop through all off-screen events 116 + $sequence_start++; 117 + $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); 118 + $date = $datetime->modify($modify_key)->format('U'); 119 + } 120 + 121 + $start = $this->rangeBegin; 122 + } else { 123 + $start = $event->getDateFrom(); 124 + } 125 + 126 + $date = $start; 127 + $start_datetime = PhabricatorTime::getDateTimeFromEpoch( 128 + $start, 129 + $viewer); 130 + 131 + if ($this->rangeEnd) { 132 + $end = $this->rangeEnd; 133 + $instance_count = $sequence_start; 134 + 135 + while ($date < $end) { 136 + $instance_count++; 137 + $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); 138 + $datetime->modify($modify_key); 139 + $date = $datetime->format('U'); 140 + } 141 + } else { 142 + $instance_count = $this->getRawResultLimit(); 143 + } 144 + 145 + $sequence_start = max(1, $sequence_start); 146 + 147 + $max_sequence = $sequence_start + $instance_count; 148 + for ($index = $sequence_start; $index < $max_sequence; $index++) { 149 + $events[] = $event->generateNthGhost($index, $viewer); 150 + } 151 + } 152 + } 153 + 89 154 return $events; 90 155 } 91 156 ··· 122 187 if ($this->rangeBegin) { 123 188 $where[] = qsprintf( 124 189 $conn_r, 125 - 'event.dateTo >= %d', 190 + 'event.dateTo >= %d OR event.isRecurring = 1', 126 191 $this->rangeBegin); 127 192 } 128 193
+9 -6
src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php
··· 50 50 } 51 51 52 52 public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { 53 - $query = id(new PhabricatorCalendarEventQuery()); 53 + $query = id(new PhabricatorCalendarEventQuery()) 54 + ->setGenerateGhosts(true); 54 55 $viewer = $this->requireViewer(); 55 56 $timezone = new DateTimeZone($viewer->getTimezoneIdentifier()); 56 57 ··· 132 133 $query->withCreatorPHIDs($creator_phids); 133 134 } 134 135 135 - $is_cancelled = $saved->getParameter('isCancelled'); 136 + $is_cancelled = $saved->getParameter('isCancelled', 'active'); 137 + 136 138 switch ($is_cancelled) { 137 139 case 'active': 138 140 $query->withIsCancelled(false); ··· 305 307 $viewer = $this->requireViewer(); 306 308 $list = new PHUIObjectItemListView(); 307 309 foreach ($events as $event) { 308 - $href = '/E'.$event->getID(); 310 + // $href = '/E'.$event->getID(); 309 311 $from = phabricator_datetime($event->getDateFrom(), $viewer); 310 312 $to = phabricator_datetime($event->getDateTo(), $viewer); 311 313 $creator_handle = $handles[$event->getUserPHID()]; 312 314 313 315 $item = id(new PHUIObjectItemView()) 314 316 ->setHeader($event->getName()) 315 - ->setHref($href) 317 + ->setHref($event->getURI()) 316 318 ->addByline(pht('Creator: %s', $creator_handle->renderLink())) 317 319 ->addAttribute(pht('From %s to %s', $from, $to)) 318 320 ->addAttribute(id(new PhutilUTF8StringTruncator()) ··· 371 373 $event->setUserPHID($status->getUserPHID()); 372 374 $event->setDescription(pht('%s (%s)', $name_text, $status_text)); 373 375 $event->setName($status_text); 374 - $event->setEventID($status->getID()); 376 + $event->setURI($status->getURI()); 377 + // $event->setEventID($status->getID()); 375 378 $event->setViewerIsInvited($viewer_is_invited); 376 379 $month_view->addEvent($event); 377 380 } ··· 423 426 $event->setViewerIsInvited($viewer_is_invited); 424 427 425 428 $event->setName($status->getName()); 426 - $event->setURI('/'.$status->getMonogram()); 429 + $event->setURI($status->getURI()); 427 430 $day_view->addEvent($event); 428 431 } 429 432
+89 -1
src/applications/calendar/storage/PhabricatorCalendarEvent.php
··· 20 20 protected $icon; 21 21 protected $mailKey; 22 22 23 + protected $isRecurring = 0; 24 + protected $recurrenceFrequency = array(); 25 + protected $recurrenceEndDate; 26 + 27 + private $isGhostEvent = false; 28 + protected $instanceOfEventPHID; 29 + protected $sequenceIndex; 30 + 23 31 protected $viewPolicy; 24 32 protected $editPolicy; 25 33 ··· 35 43 ->setViewer($actor) 36 44 ->withClasses(array('PhabricatorCalendarApplication')) 37 45 ->executeOne(); 46 + 47 + $view_policy = null; 48 + $is_recurring = 0; 38 49 39 50 if ($mode == 'public') { 40 51 $view_policy = PhabricatorPolicies::getMostOpenPolicy(); 52 + } else if ($mode == 'recurring') { 53 + $is_recurring = true; 41 54 } else { 42 55 $view_policy = $actor->getPHID(); 43 56 } ··· 46 59 ->setUserPHID($actor->getPHID()) 47 60 ->setIsCancelled(0) 48 61 ->setIsAllDay(0) 62 + ->setIsRecurring($is_recurring) 49 63 ->setIcon(self::DEFAULT_ICON) 50 64 ->setViewPolicy($view_policy) 51 65 ->setEditPolicy($actor->getPHID()) ··· 180 194 'isAllDay' => 'bool', 181 195 'icon' => 'text32', 182 196 'mailKey' => 'bytes20', 197 + 'isRecurring' => 'bool', 198 + 'recurrenceEndDate' => 'epoch?', 199 + 'instanceOfEventPHID' => 'phid?', 200 + 'sequenceIndex' => 'uint32?', 183 201 ), 184 202 self::CONFIG_KEY_SCHEMA => array( 185 203 'userPHID_dateFrom' => array( 186 204 'columns' => array('userPHID', 'dateTo'), 187 205 ), 206 + ), 207 + self::CONFIG_SERIALIZATION => array( 208 + 'recurrenceFrequency' => self::SERIALIZATION_JSON, 188 209 ), 189 210 ) + parent::getConfiguration(); 190 211 } ··· 238 259 return true; 239 260 } 240 261 262 + public function getIsGhostEvent() { 263 + return $this->isGhostEvent; 264 + } 265 + 266 + public function setIsGhostEvent($is_ghost_event) { 267 + $this->isGhostEvent = $is_ghost_event; 268 + return $this; 269 + } 270 + 271 + public function generateNthGhost( 272 + $sequence_index, 273 + PhabricatorUser $actor) { 274 + 275 + $frequency = $this->getFrequencyUnit(); 276 + $modify_key = '+'.$sequence_index.' '.$frequency; 277 + 278 + $date = $this->dateFrom; 279 + $date_time = PhabricatorTime::getDateTimeFromEpoch($date, $actor); 280 + $date_time->modify($modify_key); 281 + $date = $date_time->format('U'); 282 + 283 + $duration = $this->dateTo - $this->dateFrom; 284 + 285 + $edit_policy = PhabricatorPolicies::POLICY_NOONE; 286 + 287 + $ghost_event = id(clone $this) 288 + ->setIsGhostEvent(true) 289 + ->setDateFrom($date) 290 + ->setDateTo($date + $duration) 291 + ->setIsRecurring(false) 292 + ->setRecurrenceFrequency(null) 293 + ->setInstanceOfEventPHID($this->getPHID()) 294 + ->setSequenceIndex($sequence_index) 295 + ->setEditPolicy($edit_policy); 296 + 297 + return $ghost_event; 298 + } 299 + 300 + public function getFrequencyUnit() { 301 + $frequency = idx($this->recurrenceFrequency, 'rule'); 302 + 303 + switch ($frequency) { 304 + case 'daily': 305 + return 'day'; 306 + case 'weekly': 307 + return 'week'; 308 + case 'monthly': 309 + return 'month'; 310 + case 'yearly': 311 + return 'yearly'; 312 + default: 313 + return 'day'; 314 + } 315 + } 316 + 317 + public function getURI() { 318 + $uri = '/'.$this->getMonogram(); 319 + if ($this->isGhostEvent) { 320 + $uri = $uri.'/'.$this->sequenceIndex; 321 + } 322 + return $uri; 323 + } 324 + 241 325 /* -( Markup Interface )--------------------------------------------------- */ 242 326 243 327 ··· 307 391 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 308 392 // The owner of a task can always view and edit it. 309 393 $user_phid = $this->getUserPHID(); 394 + if ($this->isGhostEvent) { 395 + return false; 396 + } 310 397 if ($user_phid) { 311 398 $viewer_phid = $viewer->getPHID(); 312 399 if ($viewer_phid == $user_phid) { ··· 328 415 329 416 public function describeAutomaticCapability($capability) { 330 417 return pht('The owner of an event can always view and edit it, 331 - and invitees can always view it.'); 418 + and invitees can always view it, except if the event is an 419 + instance of a recurring event.'); 332 420 } 333 421 334 422 /* -( PhabricatorApplicationTransactionInterface )------------------------- */
+34
src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php
··· 12 12 const TYPE_ICON = 'calendar.icon'; 13 13 const TYPE_INVITE = 'calendar.invite'; 14 14 15 + const TYPE_RECURRING = 'calendar.recurring'; 16 + const TYPE_FREQUENCY = 'calendar.frequency'; 17 + const TYPE_RECURRENCE_END_DATE = 'calendar.recurrenceenddate'; 18 + 19 + const TYPE_INSTANCE_OF_EVENT = 'calendar.instanceofevent'; 20 + const TYPE_SEQUENCE_INDEX = 'calendar.sequenceindex'; 21 + 15 22 const MAILTAG_RESCHEDULE = 'calendar-reschedule'; 16 23 const MAILTAG_CONTENT = 'calendar-content'; 17 24 const MAILTAG_OTHER = 'calendar-other'; ··· 38 45 case self::TYPE_DESCRIPTION: 39 46 case self::TYPE_CANCEL: 40 47 case self::TYPE_ALL_DAY: 48 + case self::TYPE_RECURRING: 49 + case self::TYPE_FREQUENCY: 50 + case self::TYPE_RECURRENCE_END_DATE: 51 + case self::TYPE_INSTANCE_OF_EVENT: 52 + case self::TYPE_SEQUENCE_INDEX: 41 53 $phids[] = $this->getObjectPHID(); 42 54 break; 43 55 case self::TYPE_INVITE: ··· 60 72 case self::TYPE_CANCEL: 61 73 case self::TYPE_ALL_DAY: 62 74 case self::TYPE_INVITE: 75 + case self::TYPE_RECURRING: 76 + case self::TYPE_FREQUENCY: 77 + case self::TYPE_RECURRENCE_END_DATE: 78 + case self::TYPE_INSTANCE_OF_EVENT: 79 + case self::TYPE_SEQUENCE_INDEX: 63 80 return ($old === null); 64 81 } 65 82 return parent::shouldHide(); ··· 75 92 case self::TYPE_DESCRIPTION: 76 93 case self::TYPE_ALL_DAY: 77 94 case self::TYPE_CANCEL: 95 + case self::TYPE_RECURRING: 96 + case self::TYPE_FREQUENCY: 97 + case self::TYPE_RECURRENCE_END_DATE: 98 + case self::TYPE_INSTANCE_OF_EVENT: 99 + case self::TYPE_SEQUENCE_INDEX: 78 100 return 'fa-pencil'; 79 101 break; 80 102 case self::TYPE_INVITE: ··· 231 253 } 232 254 } 233 255 return $text; 256 + case self::TYPE_RECURRING: 257 + case self::TYPE_FREQUENCY: 258 + case self::TYPE_RECURRENCE_END_DATE: 259 + case self::TYPE_INSTANCE_OF_EVENT: 260 + case self::TYPE_SEQUENCE_INDEX: 261 + return pht('Recurring event has been updated'); 234 262 } 235 263 return parent::getTitle(); 236 264 } ··· 411 439 } 412 440 } 413 441 return $text; 442 + case self::TYPE_RECURRING: 443 + case self::TYPE_FREQUENCY: 444 + case self::TYPE_RECURRENCE_END_DATE: 445 + case self::TYPE_INSTANCE_OF_EVENT: 446 + case self::TYPE_SEQUENCE_INDEX: 447 + return pht('Recurring event has been updated'); 414 448 } 415 449 416 450 return parent::getTitleForFeed();
+1 -1
src/view/phui/calendar/PHUICalendarListView.php
··· 89 89 $content = javelin_tag( 90 90 'a', 91 91 array( 92 - 'href' => '/E'.$event->getEventID(), 92 + 'href' => $event->getURI(), 93 93 'sigil' => 'has-tooltip', 94 94 'meta' => array( 95 95 'tip' => $tip,
+2 -2
webroot/rsrc/css/phui/phui-form.css
··· 140 140 color: {$lightgreytext}; 141 141 } 142 142 143 - select[disabled="disabled"], 144 - input[disabled="disabled"] { 143 + select[disabled], 144 + input[disabled] { 145 145 opacity: 0.5; 146 146 }
+14
webroot/rsrc/js/application/calendar/behavior-recurring-edit.js
··· 1 + /** 2 + * @provides javelin-behavior-recurring-edit 3 + */ 4 + 5 + 6 + JX.behavior('recurring-edit', function(config) { 7 + var checkbox = JX.$(config.isRecurring); 8 + JX.DOM.listen(checkbox, 'change', null, function() { 9 + var frequency = JX.$(config.frequency); 10 + 11 + frequency.disabled = checkbox.checked ? false : true; 12 + }); 13 + 14 + });