1<?php
2/*
3 * Copyright Intermesh BV.
4 *
5 * This file is part of Group-Office. You should have received a copy of the
6 * Group-Office license along with Group-Office. See the file /LICENSE.TXT
7 *
8 * If you have questions write an e-mail to info@intermesh.nl
9 */
10
11namespace GO\Tasks\Model;
12use Sabre;
13
14/**
15 * The Task model
16 *
17 * @package GO.modules.Tasks
18 * @version $Id: Task.php 7607 2011-09-20 10:05:23Z <<USERNAME>> $
19 * @copyright Copyright Intermesh BV.
20 * @author <<FIRST_NAME>> <<LAST_NAME>> <<EMAIL>>@intermesh.nl
21 *
22 * @property int $id
23 * @property String $uuid
24 * @property int $tasklist_id
25 * @property int $user_id
26 * @property int $ctime
27 * @property int $mtime
28 * @property int $muser_id
29 * @property int $start_time
30 * @property int $due_time
31 * @property int $completion_time
32 * @property String $name
33 * @property String $description
34 * @property String $status
35 * @property int $repeat_end_time
36 * @property int $reminder
37 * @property String $rrule
38 * @property int $files_folder_id
39 * @property int $category_id
40 * @property int $priority
41 * @property int $project_id
42 * @property int $percentage_complete
43 */
44class Task extends \GO\Base\Db\ActiveRecord {
45
46	use \go\core\orm\CustomFieldsTrait;
47
48	const STATUS_NEEDS_ACTION = "NEEDS-ACTION";
49	const STATUS_COMPLETED = "COMPLETED";
50	const STATUS_ACCEPTED = "ACCEPTED";
51	const STATUS_DECLINED = "DECLINED";
52	const STATUS_TENTATIVE = "TENTATIVE";
53	const STATUS_DELEGATED = "DELEGATED";
54	const STATUS_IN_PROCESS = "IN-PROCESS";
55
56	const PRIORITY_LOW = 0;
57	const PRIORITY_NORMAL = 1;
58	const PRIORITY_HIGH = 2;
59
60	/**
61	 * Returns a static model of itself
62	 *
63	 * @param String $className
64	 * @return Task
65	 */
66	public static function model($className=__CLASS__)
67	{
68		return parent::model($className);
69	}
70
71	protected function init() {
72		$this->columns['name']['required']=true;
73		$this->columns['tasklist_id']['required']=true;
74
75		$this->columns['start_time']['gotype']='unixdate';
76		$this->columns['due_time']['gotype']='unixdate';
77
78		$this->columns['due_time']['greaterorequal']='start_time';
79
80		$this->columns['completion_time']['gotype']='unixdate';
81		$this->columns['repeat_end_time']['gotype']='unixdate';
82		$this->columns['reminder']['gotype']='unixtimestamp';
83		parent::init();
84	}
85
86
87	public function getUri() {
88		if(isset($this->_setUri)) {
89			return $this->_setUri;
90		}
91
92		return str_replace('/','+',$this->uuid).'-'.$this->id;
93	}
94
95	private $_setUri;
96
97	public function setUri($uri) {
98		$this->_setUri = $uri;
99	}
100
101	public function getETag() {
102		return '"' . date('Ymd H:i:s', $this->mtime). '-'.$this->id.'"';
103	}
104
105	protected function getLocalizedName() {
106		return \GO::t("Task", "tasks");
107	}
108
109	public function tableName() {
110		return 'ta_tasks';
111	}
112
113	public function aclField() {
114		return 'tasklist.acl_id';
115	}
116
117	public function hasFiles(){
118		return true;
119	}
120
121	public function hasLinks() {
122		return true;
123	}
124
125	public function customfieldsModel(){
126		return "GO\Tasks\Customfields\Model\Task";
127	}
128
129	public function relations() {
130		return array(
131				'tasklist' => array('type' => self::BELONGS_TO, 'model' => 'GO\Tasks\Model\Tasklist', 'field' => 'tasklist_id', 'delete' => false),
132				'category' => array('type' => self::BELONGS_TO, 'model' => 'GO\Tasks\Model\Category', 'field' => 'category_id', 'delete' => false),
133				'project2' => array('type' => self::BELONGS_TO, 'model' => 'GO\Projects2\Model\Project', 'field' => 'project_id', 'delete' => false)
134				);
135	}
136
137	protected function getCacheAttributes() {
138		$tasklist = empty($this->tasklist) ? '' :$this->tasklist->name;
139
140		$description = $tasklist;
141
142		 if(!empty($this->description) ){
143			$description .= ', ' . $this->description;
144		 }
145
146		return array('name'=>$this->name, 'description'=>$description, 'mtime'=>$this->due_time);
147	}
148
149	public function beforeSave() {
150		if($this->isModified('status'))
151			$this->setCompleted($this->status==Task::STATUS_COMPLETED, false);
152
153		return parent::beforeSave();
154	}
155
156	public function afterSave($wasNew) {
157
158		// task is done
159		if($this->isModified('status') && $this->status == 'COMPLETED') {
160			$this->deleteReminders();
161		}elseif($this->isModified('reminder')) {
162			$this->deleteReminders();
163			if($this->reminder>0) {
164				if($this->reminder>time() && $this->status!='COMPLETED')
165					$this->addReminder($this->name, $this->reminder, $this->tasklist->user_id);
166			}
167		}elseif($this->isModified('user_id')) {
168			// other user id
169			$this->deleteReminders();
170			$this->addReminder($this->name, $this->reminder, $this->tasklist->user_id);
171		}
172
173
174		if($this->isModified('project_id') && !empty($this->project2))
175			$this->link($this->project2);
176
177		if($this->isModified()) {
178			Tasklist::versionUp($this->tasklist_id);
179		}
180
181		return parent::afterSave($wasNew);
182	}
183
184//	public function afterLink(\GO\Base\Db\ActiveRecord $model, $isSearchCacheModel, $description = '', $this_folder_id = 0, $model_folder_id = 0, $linkBack = true) {
185//		throw new \Exception();
186//		$modelName = $isSearchCacheModel ? $model->model_name : $model->className;
187//		$modelId = $isSearchCacheModel ? $model->model_id : $model->id;
188//		echo $modelName;
189//		if($modelName=="GO\Projects\Model\Project")
190//		{
191//			$this->project_id=$modelId;
192//			$this->save();
193//		}
194//
195//
196//		return parent::afterLink($model, $isSearchCacheModel, $description, $this_folder_id, $model_folder_id, $linkBack);
197//	}
198
199	protected function afterDelete() {
200		$this->deleteReminders();
201
202		Tasklist::versionUp($this->tasklist_id);
203		return parent::afterDelete();
204	}
205
206
207	protected function afterDbInsert() {
208		if(empty($this->uuid)){
209			$this->uuid = \GO\Base\Util\UUID::create('task', $this->id);
210			return true;
211		}else
212		{
213			return false;
214		}
215	}
216
217	/**
218	 * Find all tasks that you are going to work on today
219	 * @param $date unix timestamp
220	 * @param $tasklist_id the task list to search in
221	 * @return ActiveStatement
222	 */
223	static public function findByDate($date, $tasklist_id=null) {
224		$date = \GO\Base\Util\Date::clear_time($date);
225		$criteria = \GO\Base\Db\FindCriteria::newInstance();
226		if(!empty($tasklist_id))
227			$criteria->addCondition('tasklist_id', $tasklist_id);
228		$criteria1 = \GO\Base\Db\FindCriteria::newInstance()
229				->addCondition('start_time', $date+24*3600, '<')
230				->addCondition('start_time', $date, '>=');
231		$criteria2 = \GO\Base\Db\FindCriteria::newInstance()
232				->addCondition('due_time', $date+24*3600, '<')
233				->addCondition('due_time', $date, '>=');
234		$tasks = \GO\Tasks\Model\Task::model()->find(\GO\Base\Db\FindParams::newInstance()->criteria(
235				$criteria->mergeWith($criteria1->mergeWith($criteria2, false), true))
236		);
237		return $tasks;
238	}
239
240	/**
241	 * Set the task to completed or not completed.
242	 *
243	 * @param Boolean $complete
244	 * @param Boolean $save
245	 */
246	public function setCompleted($complete=true, $save=true) {
247		if($complete) {
248			$this->completion_time = time();
249			$this->status=Task::STATUS_COMPLETED;
250			$this->percentage_complete=100;
251			$this->_recur();
252			$this->rrule='';
253		} else {
254
255			if($this->percentage_complete==100)
256				$this->percentage_complete=0;
257
258			$this->completion_time = 0;
259
260			if($this->status==Task::STATUS_COMPLETED)
261				$this->status=Task::STATUS_NEEDS_ACTION;
262		}
263
264		if($save)
265			$this->save();
266	}
267
268	/**
269	 * Creates the new Recurring task when the rrule is not empty
270	 */
271	private function _recur(){
272		if(!empty($this->rrule)) {
273
274			$rrule = new \GO\Base\Util\Icalendar\Rrule();
275			$rrule->readIcalendarRruleString($this->due_time, $this->rrule);
276
277			$nextDueTime = $rrule->getNextRecurrence($this->due_time+1);
278
279			if($nextDueTime){
280
281				$data = array(
282					'completion_time'=>0,
283					'start_time'=>$nextDueTime-$this->due_time+$this->start_time,
284					'due_time'=>$nextDueTime,
285					'status'=>Task::STATUS_NEEDS_ACTION,
286					'percentage_complete'=>0
287				);
288
289				// If a reminder is set, then calculate the difference between the start dates of the old and the new task.
290				// Then add that difference to the reminder time for the new event. (So the reminder will also move forward)
291				if(!empty($this->reminder)){
292					$diff = $data['start_time'] - $this->start_time;
293					$data['reminder'] = $this->reminder + $diff;
294				}
295
296				$dup = $this->duplicate($data);
297
298				$this->copyLinks($dup);
299			}
300		}
301	}
302
303	/**
304	 * The files module will use this function.
305	 */
306	public function buildFilesPath() {
307
308		return 'tasks/' . \GO\Base\Fs\Base::stripInvalidChars($this->tasklist->name) . '/' . date('Y', $this->due_time) . '/' . \GO\Base\Fs\Base::stripInvalidChars($this->name).' ('.$this->id.')';
309	}
310
311	public function defaultAttributes() {
312		$settings = Settings::model()->getDefault(\GO::user());
313		$defaultTasklist = Tasklist::model()->findByPk($settings->default_tasklist_id);
314		if(empty($defaultTasklist)) {
315			$oldPermissions = \GO::setIgnoreAclPermissions(true);
316			$defaultTasklist = new Tasklist();
317			$defaultTasklist->name = \GO::user()->name;
318			$defaultTasklist->user_id = \GO::user()->id;
319			if($defaultTasklist->save()) {
320				$settings->default_tasklist_id=$defaultTasklist->id;
321				$settings->save();
322			}
323			\GO::setIgnoreAclPermissions($oldPermissions);
324		}
325
326		$defaults = array(
327				'status' => Task::STATUS_NEEDS_ACTION,
328				//'remind' => $settings->remind,
329				'start_time'=> time(),
330				'due_time'=> time(),
331				'tasklist_id'=>$defaultTasklist->id,
332				//'reminder' =>$this->getDefaultReminder(time())
333		);
334		$defaults['reminder']=$this->getDefaultReminder(time());
335
336		return $defaults;
337	}
338
339	public function getDefaultReminder($startTime){
340		$settings = Settings::model()->getDefault(\GO::user());
341
342		if(!$settings->remind){
343			return 0;
344		}
345
346		$tmp = \GO\Base\Util\Date::date_add($startTime, - $settings->reminder_days);
347
348		// Set default to 8:00 when reminder_time is not set.
349		$rtime = empty($settings->reminder_time) ? "08:00" : $settings->reminder_time;
350		$dateString = date('Y-m-d', $tmp).' '.$rtime;
351
352		$time = strtotime($dateString);
353		return $time;
354	}
355
356
357	/**
358	 * Get vcalendar data for an *.ics file.
359	 *
360	 * @return StringHelper
361	 */
362	public function toICS() {
363
364		$c = new \GO\Base\VObject\VCalendar();
365		$c->add(new \GO\Base\VObject\VTimezone());
366		$c->add($this->toVObject());
367		return $c->serialize();
368	}
369
370	public function toVCS(){
371		$c = new \GO\Base\VObject\VCalendar();
372		$vobject = $this->toVObject('');
373		$c->add($vobject);
374
375		\GO\Base\VObject\Reader::convertICalendarToVCalendar($c);
376
377		return $c->serialize();
378	}
379
380
381	/**
382	 * Get this task as a VObject. This can be turned into a vcalendar file data.
383	 *
384	 * @return Sabre\VObject\Component
385	 */
386	public function toVObject(){
387
388		$calendar = new Sabre\VObject\Component\VCalendar();
389		$e=$calendar->createComponent('VTODO');
390
391		$e->uid=$this->uuid;
392
393		$e->add('dtstamp', new \DateTime("now", new \DateTimeZone('UTC')));
394
395		$mtimeDateTime = new \DateTime('@'.$this->mtime);
396		$mtimeDateTime->setTimezone(new \DateTimeZone('UTC'));
397		$e->add('LAST-MODIFIED', $mtimeDateTime);
398
399		$ctimeDateTime = new \DateTime('@'.$this->mtime);
400		$ctimeDateTime->setTimezone(new \DateTimeZone('UTC'));
401		$e->add('created', $ctimeDateTime);
402
403		$e->summary = $this->name;
404
405		$e->status = $this->status;
406
407		$dateType = "DATE";
408
409		if(!empty($this->start_time)) {
410			$e->add('dtstart', \GO\Base\Util\Date\DateTime::fromUnixtime($this->start_time), array('VALUE'=>$dateType));
411		}
412
413		$e->add('due', \GO\Base\Util\Date\DateTime::fromUnixtime($this->due_time), array('VALUE'=>$dateType));
414
415
416
417		if($this->completion_time>0){
418			$e->add('completed', \GO\Base\Util\Date\DateTime::fromUnixtime($this->completion_time), array('VALUE'=>$dateType));
419		}
420
421		if(!empty($this->percentage_complete))
422			$e->add('percent-complete',$this->percentage_complete);
423
424		if(!empty($this->description))
425			$e->description=$this->description;
426
427		//todo exceptions
428		if(!empty($this->rrule)){
429			$e->rrule=str_replace('RRULE:','',$this->rrule);
430		}
431
432		switch($this->priority) {
433			case self::PRIORITY_LOW:
434				$e->priority = 9; break;
435			case self::PRIORITY_HIGH:
436				$e->priority = 1; break;
437			default: $e->priority = 5;
438		}
439
440		if($this->reminder>0){
441
442			$a=$calendar->createComponent('VALARM');
443
444//			BEGIN:VALARM
445//ACTION:DISPLAY
446//TRIGGER;VALUE=DURATION:-PT5M
447//DESCRIPTION:Default Mozilla Description
448//END:VALARM
449
450			$a->action='DISPLAY';
451			$a->add('trigger',date('Ymd\THis', $this->reminder), array('value'=>'DATE-TIME'));
452			$a->description="Alarm";
453
454
455			//for funambol compatibility, the \GO\Base\VObject\Reader class use this to convert it to a vcalendar 1.0 aalarm tag.
456			$e->{"X-GO-REMINDER-TIME"}=date('Ymd\THis', $this->reminder);
457			$e->add($a);
458		}
459
460		return $e;
461	}
462
463
464	/**
465	 * Import a task from a VObject
466	 *
467	 * @param Sabre\VObject\Component $vobject
468	 * @param array $attributes Extra attributes to apply to the task. Raw values should be past. No input formatting is applied.
469	 * @return Task
470	 */
471	public function importVObject(Sabre\VObject\Component $vobject, $attributes=array()){
472		//$event = new \GO\Calendar\Model\Event();
473
474		$this->uuid = (string) $vobject->uid;
475		$this->name = (string) $vobject->summary;
476		$this->description = (string) $vobject->description;
477
478		if(!empty($vobject->dtstart))
479			$this->start_time = $vobject->dtstart->getDateTime()->format('U');
480
481		if(!empty($vobject->dtend)){
482			$this->due_time = $vobject->dtend->getDateTime()->format('U');
483
484			if(empty($vobject->dtstart))
485				$this->start_time=$this->due_time;
486		}
487
488		if(!empty($vobject->due)){
489			$this->due_time = $vobject->due->getDateTime()->format('U');
490		}
491		if(empty($vobject->dtstart)){
492			$this->start_time = 0;
493		}
494
495		if($vobject->dtstamp)
496			$this->mtime=$vobject->dtstamp->getDateTime()->format('U');
497
498		if(empty($this->due_time))
499			$this->due_time=time();
500
501		if($vobject->rrule){
502			$rrule = new \GO\Base\Util\Icalendar\Rrule();
503			$rrule->readIcalendarRruleString($this->start_time, (string) $vobject->rrule);
504			$rrule->shiftDays(false);
505			$this->rrule = $rrule->createRrule();
506
507			if(isset($rrule->until))
508				$this->repeat_end_time = $rrule->until;
509		}
510
511		//var_dump($vobject->status);
512		if($vobject->status)
513			$this->status=(string) $vobject->status;
514
515		if($vobject->duration){
516			$duration = \GO\Base\VObject\Reader::parseDuration($vobject->duration);
517			$this->due_time = $this->start_time+$duration;
518		}
519
520		if(!empty($vobject->priority))
521		{
522			if((string) $vobject->priority>5)
523			{
524				$this->priority=self::PRIORITY_LOW;
525			}elseif((string) $vobject->priority<3)
526			{
527				$this->priority=self::PRIORITY_HIGH;
528			}else
529			{
530				$this->priority=self::PRIORITY_NORMAL;
531			}
532		}
533
534		if(!empty($vobject->completed)){
535			$this->completion_time=$vobject->completed->getDateTime()->format('U');
536			$this->status='COMPLETED';
537		}else
538		{
539			if(empty($vobject->status)) {
540				$this->status = self::STATUS_NEEDS_ACTION;
541			}
542			$this->completion_time=0;
543		}
544
545		if(!empty($vobject->{"percent-complete"}))
546			$this->percentage_complete=(string) $vobject->{"percent-complete"};
547
548
549		if($this->status=='COMPLETED' && empty($this->completion_time))
550			$this->completion_time=time();
551
552		$this->reminder=0;
553		if($vobject->valarm && $vobject->valarm->trigger){
554			$date = $vobject->valarm->getEffectiveTriggerTime();
555			if($date) {
556				$this->reminder = $date->format('U');
557			}
558		}
559
560		$this->setAttributes($attributes, false);
561		$this->cutAttributeLengths();
562		if($this->due_time < $this->start_time)
563			$this->due_time = $this->start_time;
564		$this->save();
565
566		return $this;
567	}
568
569	/**
570	 * Check is this task is over due.
571	 *
572	 * @return boolean
573	 */
574	public function isLate(){
575		$today = date("Ymd");
576		return $this->status!='COMPLETED' && date("Ymd",$this->due_time) < $today;
577	}
578
579	public function isActive() {
580		$today = date("Ymd");
581		return (date("Ymd",$this->start_time) <= $today && date("Ymd",$this->due_time) >= $today);
582	}
583
584	public function getProjectName() {
585		if(!$this->project2) {
586			return null;
587		}
588		$parts = explode('/', $this->project2->path);
589
590		$name = array_pop($parts);
591
592		$next = array_pop($parts);
593
594		return $next ? $next . '/' . $name : $name;
595	}
596}
597