1<?php 2 3/* 4 * Copyright Intermesh BV. 5 * 6 * This file is part of Group-Office. You should have received a copy of the 7 * Group-Office license along with Group-Office. See the file /LICENSE.TXT 8 * 9 * If you have questions write an e-mail to info@intermesh.nl 10 */ 11 12/** 13 * The Participant model 14 * 15 * @package GO.modules.Calendar 16 * @version $Id: Participant.php 7607 2011-09-28 10:31:03Z <<USERNAME>> $ 17 * @copyright Copyright Intermesh BV. 18 * @author <<FIRST_NAME>> <<LAST_NAME>> <<EMAIL>>@intermesh.nl 19 * 20 * @property int $id 21 * @property int $event_id 22 * @property string $name 23 * @property string $email 24 * @property int $user_id 25 * @property int $contact_id 26 * @property int $status 27 * @property string $last_modified 28 * @property int $is_organizer 29 * @property string $role 30 * 31 * @property \GO\Calendar\Model\Event $event 32 * @property string $statusName; 33 * 34 * 35 */ 36 37namespace GO\Calendar\Model; 38 39use go\core\model\Link; 40 41class Participant extends \GO\Base\Db\ActiveRecord { 42 43 const STATUS_TENTATIVE = "TENTATIVE"; 44 const STATUS_DECLINED = "DECLINED"; 45 const STATUS_ACCEPTED = "ACCEPTED"; 46 const STATUS_PENDING = "NEEDS-ACTION"; 47 48 public $notifyOrganizer=false; 49 50 51 public $updateRelatedParticipants=true; 52 53 public $dontCreateEvent = false; 54 55 56 public $notifyRecurrenceTime=false; 57 58 /** 59 * Returns a static model of itself 60 * 61 * @param String $className 62 * @return Participant 63 */ 64 public static function model($className = __CLASS__) { 65 return parent::model($className); 66 } 67 68 public function validate() { 69 if (empty($this->name)) 70 $this->name = $this->email; 71 72 return parent::validate(); 73 } 74 75 /** 76 * Enable this function if you want this model to check the acl's automatically. 77 */ 78 // public function aclField(){ 79 // return 'acl_id'; 80 // } 81 82 /** 83 * Returns the table name 84 */ 85 public function tableName() { 86 return 'cal_participants'; 87 } 88 89 /** 90 * Here you can define the relations of this model with other models. 91 * See the parent class for a more detailed description of the relations. 92 */ 93 public function relations() { 94 return array( 95 'event' => array('type' => self::BELONGS_TO, 'model' => 'GO\Calendar\Model\Event', 'field' => 'event_id') 96 ); 97 } 98 99 /** 100 * Check if the participant is available. 101 * 102 * Returns a questionmark if the particiant is not a user. 103 * 104 * @param int $start_time If empty then the related event will be used. 105 * @param int $end_time If empty then the related event will be used. 106 * 107 * @return boolean/? 108 */ 109 public function isAvailable($start_time = false, $end_time = false) { 110 if (!$this->hasFreeBusyAccess()) { 111 return '?'; 112 } else { 113 114 if (!$start_time && $this->event) 115 $start_time = $this->event->start_time; 116 117 if (!$end_time && $this->event) 118 $end_time = $this->event->end_time; 119 120 if ($start_time && $end_time) 121 return self::userIsAvailable($start_time, $end_time, $this->user_id, $this->event); 122 else 123 return '?'; 124 } 125 } 126 127 /** 128 * Check if the current user has free busy access. 129 * 130 * @return boolean 131 */ 132 public function hasFreeBusyAccess() { 133 134 $permission=!empty($this->user_id); 135 if($permission && \GO::modules()->isInstalled("freebusypermissions")){ 136 $permission = \GO\Freebusypermissions\FreebusypermissionsModule::hasFreebusyAccess(\GO::user()->id, $this->user_id); 137 } 138 return $permission; 139 } 140 141 /** 142 * Check if a user has events between two given times. 143 * 144 * @param type $periodStartTime 145 * @param type $periodEndTime 146 * @param type $userId 147 * @param type $ignoreEvent 148 * @return boolean 149 */ 150 public static function userIsAvailable($periodStartTime, $periodEndTime, $userId, $ignoreEvent = false) { 151 152 $findParams = \GO\Base\Db\FindParams::newInstance() 153 ->ignoreAcl(); 154 155 $joinCriteria = \GO\Base\Db\FindCriteria::newInstance() 156 ->addRawCondition('t.calendar_id', 'c.id'); 157 158 $findParams->join(Calendar::model()->tableName(), $joinCriteria, 'c'); 159 160 $findParams->getCriteria()->addCondition('user_id', $userId, '=', 'c'); 161 162 if ($ignoreEvent) { 163 $findParams->getCriteria() 164 ->addModel(Event::model()) 165 ->addCondition('id', $ignoreEvent->id, '!=') 166 ->addCondition('uuid', $ignoreEvent->uuid, '!=') 167 ; 168 } 169 170 $events = Event::model()->findCalculatedForPeriod($findParams, $periodStartTime, $periodEndTime, true); 171 172 foreach ($events as $event) { 173 \GO::debug($event->getName()); 174 } 175 176 return count($events) == 0; 177 } 178 179 /** 180 * Get free busy information in specified interval blocks of minutes 181 * 182 * @param int $starttime 183 * @param int $endtime 184 * @param int $intervalMinutes 185 * @return array 186 */ 187 public function getFreeBusyInfo($starttime, $endtime, $intervalMinutes=15){ 188 189 190 $freebusy=array(); 191 $startTimeMin = $starttime/60; 192 $endTimeMin = $endtime/60; 193 194 for($i=$startTimeMin;$i<$endTimeMin;$i+=$intervalMinutes) { 195 $freebusy[$i-$startTimeMin]=0; 196 } 197 198 if(empty($this->user_id)) 199 return $freebusy; 200 201 $findParams = \GO\Base\Db\FindParams::newInstance() 202 ->ignoreAcl(); 203 204 $joinCriteria = \GO\Base\Db\FindCriteria::newInstance() 205 ->addRawCondition('t.calendar_id', 'c.id'); 206 207 $findParams->join(Calendar::model()->tableName(), $joinCriteria, 'c'); 208 209 $findParams->getCriteria()->addCondition('user_id', $this->user_id, '=', 'c'); 210 211 $events = Event::model()->findCalculatedForPeriod($findParams, $starttime, $endtime, true); 212 213 214 215 foreach($events as $localEvent) { 216 217// echo $localEvent->getName()."\n"; 218 219 if($localEvent->getEvent()->id!=$this->event_id) { 220 221 $eventEndTime=$localEvent->getAlternateEndTime(); 222 if($eventEndTime > $endtime) { 223 $eventEndTime=$endtime-1; 224 } 225 226 $eventStartTime=$localEvent->getAlternateStartTime(); 227 if($eventStartTime < $starttime) { 228 $eventStartTime=$starttime; 229 } 230 231 $event_start = getdate($eventStartTime); 232 $event_end = getdate($eventEndTime); 233 234 $mod = $event_start['minutes'] % $intervalMinutes; 235 if($mod>0) 236 $event_start['minutes']+=(15-$mod); 237 238 $mod = $event_end['minutes'] % $intervalMinutes; 239 if($mod>0) 240 $event_end['minutes']+=(15-$mod); 241 242 243 $start_minutes = $event_start['minutes']+($event_start['hours']*60); 244 $end_minutes = $event_end['minutes']+($event_end['hours']*60); 245 246// echo $start_minutes.' - > '.$end_minutes."\n"; 247 248 for($i=$start_minutes;$i<$end_minutes;$i+=$intervalMinutes) { 249 $freebusy[$i]=1; 250 } 251 } 252 } 253 return $freebusy; 254 } 255 256 public function defaultAttributes() { 257 $attr = parent::defaultAttributes(); 258 $attr['user_id'] = null; 259 return $attr; 260 } 261 262 public function getSecurityToken() { 263 return md5($this->event_id . $this->email . $this->event->ctime); 264 } 265 266 public function getStatusName() { 267 switch ($this->status) { 268 case self::STATUS_TENTATIVE : 269 return \GO::t("Tentative", "calendar"); 270 break; 271 272 case self::STATUS_DECLINED : 273 return \GO::t("Declined", "calendar"); 274 break; 275 276 case self::STATUS_ACCEPTED : 277 return \GO::t("Accepted", "calendar"); 278 break; 279 280 default: 281 return \GO::t("Not responded yet", "calendar"); 282 break; 283 } 284 } 285 286 /** 287 * Get related participant event. UUID and user_id of calendar must match. 288 * Returns false if it doesn't exists. 289 * 290 * @return Event 291 */ 292 public function getParticipantEvent() { 293 294 if(!$this->event || !$this->user_id) 295 return false; 296 297 298 $params = \GO\Base\Db\FindParams::newInstance() 299 ->ignoreAcl() 300 ->single(); 301 302 $params->getCriteria() 303 ->addCondition('uuid', $this->event->uuid) 304 ->addCondition('start_time', $this->event->start_time) //make sure start time matches for recurring series 305 ->addCondition("exception_for_event_id", 0, $this->event->exception_for_event_id==0 ? '=' : '!='); //the master event or a single occurrence can start at the same time. Therefore we must check if exception event has a value or is 0. 306 307 308 $joinCriteria = \GO\Base\Db\FindCriteria::newInstance() 309 ->addCondition('calendar_id', 'c.id','=','t',true, true) 310 ->addCondition('user_id', $this->user_id,'=','c'); 311 312 $params->join(Calendar::model()->tableName(), $joinCriteria, 'c'); 313 314 315 return Event::model()->find($params); 316 317 } 318 319 /** 320 * Get related participant event. UUID and user_id of calendar must match. 321 * Returns false if it doesn't exists. 322 * 323 * @return Event 324 */ 325 public function getOrganizerEvent() { 326 if ($this->is_organizer) 327 return $this->event; 328 else 329 return Event::model()->findSingleByAttributes(array('uuid' => $this->event->uuid, 'is_organizer' => 1)); 330 } 331 332 /** 333 * Get's the participant's default calendar if it has one. 334 * @return Calendar 335 */ 336 public function getDefaultCalendar() { 337 if (empty($this->user_id)) 338 return false; 339 340 return Calendar::model()->findDefault($this->user_id); 341 } 342 343 public function toJsonArray($start_time = false, $end_time = false) { 344 $record = $this->getAttributes('html'); 345 346 $record['available'] = $this->isAvailable($start_time, $end_time); 347 $calendar = $this->getDefaultCalendar(); 348 $record['create_permission'] = $calendar ? $calendar->userHasCreatePermission() : false; 349 350 if($this->isNew){ 351 unset($record['id']);//otherwise it replaces new participants 352 } 353 return $record; 354 } 355 356 357 protected function afterSave($wasNew) { 358 359 360 if(!$wasNew && $this->updateRelatedParticipants && $this->isModified('status')){ 361 $stmt = $this->getRelatedParticipants(); 362 363 foreach($stmt as $participant){ 364 365 $participant->updateRelatedParticipants=false;//prevent endless loop. Because it will also process this aftersave 366 367 $participant->event->touch(); // Touch the event to update its mtime. 368 369 $participant->status=$this->status; 370 $participant->save(); 371 } 372 373 //$this->event->touch(); // Touch the event to update the modification date. 374 } 375 376 if($wasNew && $this->event->is_organizer){ 377 378 //add this participant to each existing event. 379 if (!$this->dontCreateEvent && $this->user_id > 0 && !$this->is_organizer) { 380// if ($this->user_id > 0 && !$this->is_organizer) { 381 $newEvent = $this->event->createCopyForParticipant($this); 382 } 383 384 385 $stmt = $this->event->getRelatedParticipantEvents(); 386 387 foreach($stmt as $event){ 388 if(!isset($newEvent) || !$newEvent || $event->id!=$newEvent->id){ 389 390 $p = Participant::model()->findSingleByAttributes(array( 391 'event_id'=>$event->id, 392 'email'=>$this->email 393 )); 394 if(!$p){ 395 $p = new Participant(); 396 $p->setAttributes($this->getAttributes('raw'), false); 397 $p->event_id=$event->id; 398 $p->id=null; 399 $p->save(); 400 } 401 } 402 } 403 404 405 406 if(!$this->is_organizer && $this->contact_id && \GO::config()->calendar_autolink_participants){ 407 $contact = \go\modules\community\addressbook\model\Contact::findById($this->contact_id); 408 if(!empty($contact)) { 409 Link::create($contact, $this->event); 410 } 411 } 412 } 413 414// $this->_updateEvents(); 415 416 return parent::afterSave($wasNew); 417 } 418 419 protected function afterDelete(){ 420 421 422 if($this->event && $this->event->is_organizer){ 423 $stmt = $this->getRelatedParticipants(); 424 425 foreach($stmt as $participant){ 426 if($event = $participant->getParticipantEvent()) 427 { 428 if(!$event->is_organizer && $event->calendar->userHasCreatePermission()) 429 $event->delete(true); 430 } 431 $participant->delete(); 432 } 433 } 434 435 if($this->event) { 436 $this->event->touch(); 437 } 438 439 return parent::afterDelete(); 440 } 441 442// private function _updateEvents(){ 443// 444// if(!$this->event) { 445// return; 446// } 447// // Update mtime of all events (For each participant) 448// $stmt = $this->event->getRelatedParticipantEvents(true); 449// 450// foreach($stmt as $event){ 451// $event->mtime = time(); 452// $event->save(true); 453// } 454// } 455 456 457// private function _notifyOrganizer(){ 458// 459//// if(!$sendingParticipant) 460//// throw new \Exception("Could not find your participant model"); 461// 462// $organizer = $this->event->getOrganizer(); 463// if(!$organizer) 464// throw new \Exception("Could not find organizer to send message to!"); 465// 466// $updateReponses = \GO::t("updateReponses", "calendar"); 467// $subject= sprintf($updateReponses[$this->status], $this->user->name, $this->event->name); 468// 469// 470// //create e-mail message 471// $message = \GO\Base\Mail\Message::newInstance($subject) 472// ->setFrom($this->user->email, $this->user->name) 473// ->addTo($organizer->email, $organizer->name); 474// 475// $body = '<p>'.$subject.': </p>'.$this->event->toHtml(); 476// 477// if(!$this->event->getOrganizerEvent()){ 478// //organizer is not a Group-Office user with event. We must send a message to him an ICS attachment 479// $ics=$this->event->toICS("REPLY", $this, $this->notifyRecurrenceTime); 480// $a = \Swift_Attachment::newInstance($ics, \GO\Base\Fs\File::stripInvalidChars($this->event->name) . '.ics', 'text/calendar; METHOD="REPLY"'); 481// $a->setEncoder(new \Swift_Mime_ContentEncoder_PlainContentEncoder("8bit")); 482// $a->setDisposition("inline"); 483// $message->attach($a); 484// } 485// 486// $message->setHtmlAlternateBody($body); 487// 488// \GO\Base\Mail\Mailer::newGoInstance()->send($message); 489// 490// } 491 492 493 /** 494 * Returns all participant models for this event and all the related events for a meeting. 495 * 496 * @return Participant 497 */ 498 public function getRelatedParticipants(){ 499 //update all participants with this user and event uuid in the system 500 $findParams = \GO\Base\Db\FindParams::newInstance(); 501 502 $findParams->joinModel(array( 503 'model'=>'GO\Calendar\Model\Event', 504 'localTableAlias'=>'t', //defaults to "t" 505 'localField'=>'event_id', //defaults to "id" 506 'foreignField'=>'id', //defaults to primary key of the remote model 507 'tableAlias'=>'e', //Optional table alias 508 )); 509 510 $findParams->getCriteria() 511 ->addCondition('id', $this->id, '!=') 512 ->addCondition('email', $this->email) 513 ->addCondition('uuid', $this->event->uuid,'=','e') //recurring series and participants all share the same uuid 514 ->addCondition('start_time', $this->event->start_time,'=','e') //make sure start time matches for recurring series 515 ->addCondition("exception_for_event_id", 0, $this->event->exception_for_event_id==0 ? '=' : '!=','e');//the master event or a single occurrence can start at the same time. Therefore we must check if exception event has a value or is 0. 516 517 return Participant::model()->find($findParams); 518 519 } 520 521 protected function beforeSave() { 522 523 // Check for a user with this email address 524 if($this->isNew && $this->user_id === null){ 525 $user = \GO\Base\Model\User::model()->findSingleByAttribute('email', $this->email); 526 if($user) 527 $this->user_id = $user->id; 528 } 529 530 if($this->is_organizer) 531 $this->status=self::STATUS_ACCEPTED; 532 533 return parent::beforeSave(); 534 } 535} 536