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