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