e91248ff346c1eb6e6efab5bd761dd2d733c5761
[Pman.MTrack] / DataObjects / Mtrack_ticket.php
1 <?php
2 /**
3  * Table Definition for mtrack_ticket
4  */
5 class_exists('DB_DataObject') ? '' : require_once 'DB/DataObject.php';
6
7 class Pman_MTrack_DataObjects_Mtrack_ticket extends DB_DataObject 
8 {
9     ###START_AUTOCODE
10     /* the code below is auto generated do not remove the above tag */
11
12     public $__table = 'mtrack_ticket';                   // table name
13     public $id;                              // int(11)  not_null primary_key auto_increment
14     public $summary;                         // blob(-1)  blob
15     public $description;                     // blob(-1)  blob
16     public $changelog;                       // blob(-1)  blob
17     public $created;                         // int(11)  not_null
18     public $updated;                         // int(11)  not_null
19     public $owner_id;                        // int(11)  not_null
20     public $developer_id;                        // int(11)  not_null
21
22     public $priority_id;                     // int(11)  not_null
23     
24     public $severity_id;                     // int(11)  not_null
25     public $classification_id;               // int(11)  not_null
26     public $resolution_id;                   // int(11)  not_null
27     public $cc;                              // blob(65535)  blob
28     public $status;                          // int(11)  not_null
29     public $estimated;                       // real(22)  
30     public $spent;                           // real(22)  
31     public $x_fieldname;                     // blob(65535)  blob
32     public $project_id;
33     public $milestone_id;
34     
35     public $act_now;                        // a field to flag as ready to go.
36                                             // we only allow 3 of these to
37                                             // be active at the same time..
38     
39     /* the code above is auto generated do not remove the tag below */
40     ###END_AUTOCODE
41     //checkPerm('S'/'E'/'A', $authuser) - can
42     function checkPerm($perm, $au)
43     {
44         $pr = $this->project();
45         // var_dump($pr ? $pr->name : false); var_dump($perm);
46         
47         if (($perm == 'S')  && (!$pr || ($pr->code == '*PUBLIC'))) {
48             return true;
49         }
50         if (!$au) {
51             return false;
52         }
53         // admins with all project rights.
54         if ($au->hasPerm('Core.Projects_All', 'S')) {
55             return true;
56         }
57          
58         $pd = DB_DataObject::factory('ProjectDirectory');
59         $pd->user_id = $au->id;
60         $pd->project_id = $this->project_id;
61         $pd->whereAdd("role != ''");
62         // not        
63         if ($pd->count()) {
64             return true;
65         }
66         return false;
67         
68     }
69     
70     function project()
71     {
72         if (!$this->project_id) {
73             return false;
74         }
75         $p = DB_DataObject::factory('core_project');
76         $p->get($this->project_id);
77         return $p;
78          
79     }
80     function status()
81     {
82         if (!$this->status) {
83             return false;
84         }
85         $p = DB_DataObject::factory('core_enum');
86         $p->get($this->status);
87         return $p;
88          
89     }
90     
91     function resolution()
92     {
93         if (!$this->resolution_id) {
94             return false;
95         }
96         $p = DB_DataObject::factory('core_enum');
97         $p->get($this->resolution_id);
98         return $p;
99          
100     }
101     
102      function classification()
103     {
104         if (!$this->classification_id) {
105             return false;
106         }
107         $p = DB_DataObject::factory('core_enum');
108         $p->get($this->classification_id);
109         return $p;
110          
111     }
112     
113     function developer()
114     {
115         if (!$this->developer_id) {
116             return false;
117         }
118         $p = DB_DataObject::factory('Person');
119         $p->get($this->developer_id);
120         return $p;
121         
122     }
123     
124     function applyFilters($q, $au, $roo)
125     {
126         if (isset($q['query']['ticket_change'])) {
127             return $this->ticketChangeOptions($roo,$q, $au);
128         }
129         
130         $tn = $this->tableName();
131         if (!$au) {
132             // then we only show global projects..
133            // DB_DAtaObject::DebugLevel(1);
134             $this->whereAdd(" join_project_id_id.code = '*PUBLIC' ");
135         } else if ($au->hasPerm('Core.Projects_All', 'S')) {
136             //  // if we are admin?? should we show all..
137                
138         } else {
139             //DB_DataObject::debugLevel(1);
140             // restrict to any projects that the user is involved in..
141             $pd = DB_DataObject::factory('ProjectDirectory');
142             $pd->person_id = $au->id;
143             $pd->whereAdd("role != ''");
144             
145             if (!$pd->count()) {
146                 $this->whereAdd(" join_project_id_id.code = '*PUBLIC' ");
147             
148                 // they can do what they like on all projects.
149                 // no op..
150             } else {
151                 
152                 
153                 $ar = $pd->fetchAll('project_id');
154                 $this->whereAddIn("join_project_id_id.id", $ar, 'int');
155                 
156             }
157              
158             
159         }
160         // is_deleted = 0...
161         $this->selectAdd("
162             COALESCE((
163                 SELECT
164                     count(id)
165                 FROM
166                     Images
167                 WHERE
168                     onid = mtrack_ticket.id
169                     AND
170                     ontable = 'mtrack_ticket'
171                 ), 0) as no_images
172             
173         ");
174         
175         
176         if ($au && isset($q['_options_for'])) {
177             $this->optionsForProject($roo, $q['_options_for']);
178             $this->jerr("somethings missing");
179             
180         }
181         
182         
183         
184         $jira_ref =  "(SELECT jira_key
185                     FROM
186                         mtrack_jira_object
187                     WHERE on_table='mtrack_ticket' AND on_id = {$tn}.id LIMIT 1)";
188           
189         
190         if (empty($q['_distinct'])) {
191             $tn = $this->tableName();
192             $this->selectAdd("$jira_ref as jira_ref ");
193         }    
194           
195           
196         //var_dump($q);exit;
197         if (!empty($q['query']['search'])) {
198             
199             $sq = $this->escape($q['query']['search']);
200             $cond = '';
201             if (is_numeric($q['query']['search'])) {
202                 $cond = "OR {$tn}.id = {$q['query']['search']}";
203             }
204             $this->whereAdd(" 
205                  {$tn}.summary like '%{$sq}%'
206                  OR
207                  {$tn}.description like '%{$sq}%'
208                  OR
209                  $jira_ref like '%{$sq}%'
210                 $cond
211             ");
212             
213             if (is_numeric($q['query']['search'])) {
214                 // allow quick lookup by number..
215                 
216                 return;
217             }
218             
219         }
220             
221         if (!empty($q['_developer'])) {
222             switch ($q['_developer']) {
223                 case 'me':
224                     $this->whereAdd("mtrack_ticket.developer_id = {$au->id} ");
225                     break;
226                 case 'anybody':
227                     break;
228             }
229         }
230         
231         
232         if (!empty($q['query']['viewtype'])) {
233              
234             switch ($q['query']['viewtype']) {
235                 case 'active':
236                     // hide "on hold" and closed..
237                     $this->whereAdd("join_status_id.name NOT IN(  'CLOSED') OR  {$tn}.status  = 0");
238                     $this->whereAdd("{$tn}.milestone_id = 0 OR join_milestone_id_id.on_hold = 0");
239                     break;
240                 
241                 case 'on-hold':
242                     $this->whereAdd("join_status_id.name = 'ON_HOLD' ");
243                     $this->whereAdd("{$tn}.milestone_id = 0 OR join_milestone_id_id.on_hold = 0");
244                     break;
245                 
246                 case 'my':
247                     $this->whereAdd("mtrack_ticket.developer_id = {$au->id} ");
248                     $this->whereAdd("{$tn}.milestone_id = 0 OR join_milestone_id_id.on_hold = 0");
249                     
250                 case 'me':
251                 case 'me-all':
252                     $this->whereAdd("join_status_id.name NOT IN('closed', 'CLOSED') OR  {$tn}.status  = 0 ");
253                     $this->whereAdd("mtrack_ticket.developer_id = {$au->id} ");
254                     
255                     break;
256                 
257                   
258                 case 'closed':
259                     $this->whereAdd("join_status_id.name ='closed'");
260                     
261                     break;
262                  
263                 case 'active-or-recent':
264                     $this->whereAdd("
265                             (join_status_id.name  NOT IN('closed', 'CLOSED')  OR  {$tn}.status  = 0)
266                             OR
267                             (join_status_id.name    IN('closed', 'CLOSED')  AND join_updated.changedate > NOW() - 1 MONTH  )
268                             ");
269                     $this->whereAdd("{$tn}.milestone_id = 0 OR join_milestone_id_id.on_hold = 0");
270                     break;
271                 
272                 case 'pending':
273                     $this->whereAdd("join_status_id.name IN ('REVIEW_BY_ADMIN', 'REVIEW_BY_CLIENT )");
274                     break;
275                 
276                 case 'in-progress':
277                     $this->whereAdd("join_status_id.name IN ('IN_PROGRESS' )");
278                     break;
279                     
280                 case 'all': 
281                     $this->whereAdd("1=1");
282                     break;
283                 
284                 default :
285                     $this->whereAdd("1=0");
286                 
287             }
288             
289             
290             
291         }
292         
293         if (!empty($q['_orderby_self_alloc '])) {
294             $auid = $au ? (int) $au->id : 0;
295             $this->selectAdd(" 
296                   IF(developer_id = $auid , 1, 0) as is_self_alloc         
297                              ");
298             $this->orderBy('is_self_alloc DESC');
299             
300             
301         }
302         
303         if (!empty($q['query']['include_id'])) {
304             $this->whereAdd("{$tn}.id = " . ((int) $q['query']['include_id'] ), 'OR');
305         }
306         
307         
308         /// only show unposted..
309         
310     }
311     
312     
313     function ticketChangeOptions($roo, $q, $au)
314     {
315             $et = DB_DataObject::Factory('core_enum');
316             $et->active  = 1;
317             $et->whereAddIn('etype', array(
318                     'ticketstate',
319                     'resolution',
320                     //'classification',
321                     //'severity',
322                     //'priority'
323             ),'string') ;
324            
325             
326             // the end result of this:
327             /*
328                
329                
330                ticketstate_[same]
331                ticketstate_[me] ????
332                
333                ticketstate_[... everything else except close & new ...]
334                   .. eg. open, reopened..
335                
336                // these actually close the ticket...
337                // so the do not appear if the ticket is closed...
338                resolution:
339                     
340              
341              
342             */
343             
344             if (empty($q['query']['ticket_change'])) {
345                 $roo->jerr("no ticket!");
346             }
347             
348             // find out what state the ticket is in.
349             $t = DB_DataObject::Factory('mtrack_ticket');
350             
351             if (strpos($q['query']['ticket_change'], ',')  > 0) {
352                 $t->status = 0;
353                 $t->status_name = 'xx';
354                 
355             } else {
356                 $t->autoJoin();
357                 $t->get($q['query']['ticket_change']);
358             
359             }
360             if (!$et->checkPerm('S', $au)) {
361                 $roo->jerr("permssion denied to query state of ticket");
362             }
363             
364
365             
366             //$this->debugLevel(1);
367             
368             
369             
370             $et->selectAdd("
371                 IF( id = {$t->status} , 0, 10000) +
372                 IF( etype='ticketstate'
373                     AND name='assigned' , 10, 0) +
374                 IF( etype='ticketstate' AND
375                         name !='closed'  AND  name != 'new'
376                         AND
377                         name !='CLOSED'  AND  name != 'NEW', 100, 0) +
378                 IF( etype='ticketstate' , 0 , 1000) +
379
380                 seqid
381                 AS
382                 ts_order
383                              
384             ");
385             
386             // hide stuff...
387             switch($t->status_name) {
388                 
389                 case 'new':
390                 case 'NEW':
391                     // do not show closed..
392                     $et->whereAdd("
393                         ( CONCAT(etype , '.', name) != 'ticketstate.closed')
394                         AND
395                          ( CONCAT(etype , '.', name) != 'ticketstate.reopened')
396                          AND 
397                          ( CONCAT(etype , '.', name) != 'ticketstate.CLOSED') 
398                     ");
399                     break;
400                   
401                 case 'closed':
402                 case 'CANCELLED':
403                     $et->whereAdd("
404                                     ( CONCAT(etype , '.', name) = 'ticketstate.reopened')
405                                     OR
406                                     core_enum.id = {$t->status}
407                     ");
408                     break;
409                 
410                 case 'reopened':
411                 
412                     $et->whereAdd("
413                              ( CONCAT(etype , '.', name) != 'ticketstate.closed')
414                              AND
415                             ( CONCAT(etype , '.', name) != 'ticketstate.new')
416                             AND
417                              ( CONCAT(etype , '.', name) != 'ticketstate.NEW')
418                              AND
419                            ( CONCAT(etype , '.', name) != 'ticketstate.open')
420                             ");
421                     break;
422
423                 case 'open':
424                 case 'OPEN':
425                     
426                 default:
427                     $et->whereAdd("
428                            ( CONCAT(etype , '.', name) != 'ticketstate.closed')
429                              AND
430                             ( CONCAT(etype , '.', name) != 'ticketstate.new')
431                              AND
432                            ( CONCAT(etype , '.', name) != 'ticketstate.reopened')
433                             ");
434                     break;
435                  
436             }
437             
438             $et->selectAdd("
439                 CASE WHEN etype= 'ticketstate' THEN
440                     LOWER(REPLACE(name, '_', ' '))
441                 ELSE
442                     CONCAT('closed - ', LOWER(REPLACE(name, '_', ' ')))
443                 END AS name_mixed,
444                 CASE WHEN etype= 'ticketstate' THEN
445                     display_name
446                 ELSE
447                     CONCAT('Closed - ', display_name)
448                 END AS display_name_mixed
449                 
450             ");
451             
452             
453             $et->orderBy("ts_order ASC");
454             
455             $roo->jdata( $et->fetchAll(false,false,'toArray'));
456             exit;
457             
458     }
459     
460     
461     /**
462      *-- return false to let system do default sort code.
463      **/
464     
465     function applySort($au, $sortcol, $direction)
466     {
467         // fixme smarter?
468         $direction = $direction == 'DESC' ? 'DESC' : 'ASC';
469         
470         $tn = $this->tableName();
471         switch( $sortcol ) {
472             
473             case 'id':
474             //..... others..
475                 $this->orderBy("$sortcol $direction");
476                 return;
477             
478             case 'milestone_id':
479                    $this->orderBy("
480                         case
481                             when join_status_id.display_name = 'IN_PROGRESS' then 1
482                             when join_status_id.display_name = 'REVIEW_BY_ADMIN' then -1
483                             else 0
484                         end DESC,
485                        (case when join_milestone_id_id.duedate is null then 1 else 0 end) ASC,
486                             join_milestone_id_id.duedate $direction ,
487                               {$tn}.seqid $direction ,
488                               join_milestone_id_id.name $direction
489                        ");
490                   
491                 return;
492                 
493             case 'old_style':
494                 $this->orderBy('
495                        (case when join_milestone_id_id.duedate is null then 1 else 0 end) ASC,
496                             join_milestone_id_id.duedate ASC ,
497                             join_severity_id_id.seqid ASC,
498                             join_priority_id_id.seqid ASC,
499                             join_classification_id_id.seqid ASC,
500                             join_updated_id.changedate  DESC
501                        ');
502                 return;
503             case 'priority_id':
504                 $this->orderBy("
505                         case
506                             when join_status_id.display_name = 'IN_PROGRESS' then 1
507                             when join_status_id.display_name = 'REVIEW_BY_ADMIN' then -1
508                             else 0
509                         end DESC,
510                         join_priority_id_id.seqid $direction, {$tn}.seqid ASC
511                     ");
512                 break;
513             default:
514                 // this should favour in_progress, and disfavour resolved
515                 
516                 $this->orderBy("
517                         case
518                             when join_status_id.display_name = 'IN_PROGRESS' then 1
519                             when join_status_id.display_name = 'REVIEW_BY_ADMIN' then -1
520                             else 0
521                         end DESC,
522                         join_priority_id_id.seqid ASC,
523                         {$tn}.seqid ASC   ");
524                         
525                
526                             
527                 
528                 return;
529         }
530         
531     }
532     
533     /*
534      
535      (case when join_milestone_id_id.duedate is null then 1 else 0 end),
536                             join_milestone_id_id.duedate ASC ,
537                             join_severity_id_id.seqid ASC,
538                             join_priority_id_id.seqid ASC,
539                             join_classification_id_id.seqid ASC,
540                             join_updated_id.changedate  DESC
541     */
542     
543     
544     
545     
546     
547     
548     
549     
550     
551     
552     function isOpen()
553     {
554         switch ($this->status) {
555             case 'closed':
556             case 'CLOSED':
557                 return false;
558             
559           default:
560                 return true;
561         }
562     }
563     function descriptionToHtml() /* depreciated */
564     {
565        // return MTrack_Wiki::format_to_html($this->description);
566         return htmlspecialchars($this->description);
567     }
568     
569     
570     
571     function toEmailCalcAddress($changes, $person)
572     {
573         $ff = HTML_FlexyFramework::get();
574      
575         
576         $headers['To'] = '"'. addslashes($person->name). '" <' . $person->email .'>';
577         $headers['From'] = $ff->MTrackWeb['email_address']; // could be the user who made the change...
578         
579         //if (count($from) > 1) {
580         //    $rep = array();
581         //    array_shift($from);
582          //       foreach ($from as $email) {
583          // $rep[] = make_email($email[0], $email[1]);
584         // }
585         
586         $headers['Reply-To'] =  $ff->MTrackWeb['email_address'];
587         return $headers;
588     }
589     
590     function images()
591     {
592         $i = DB_DataObject::factory('Images');
593         return $i->gather($this);
594     }
595     
596     function toEventName()
597     {
598         return $this->summary;
599         
600     }
601     
602     
603     function toEmail($person, $lasttime)
604     {
605         $ff = HTML_FlexyFramework::get(); // used to work out url to show..
606         
607         $CS = DB_DataObject::factory('mtrack_change');
608         $changes = $CS->changesSince($this,$lasttime);
609         $headers = $this->toEmailCalcAddress($changes, $person);
610         
611         $headers += array(
612             'MIME-Version' => '1.0',
613             'Content-Type' => 'text/plain; charset="UTF-8"',
614             'Content-Transfer-Encoding' => 'quoted-printable',
615         );
616     
617         
618         $mid = $this->id . '@' . php_uname('n');
619         
620         $p = $this->project();
621          
622         $subj = "[" . $p->code . ' - ' . $p->name . "] ";
623     
624         $headers['X-mtrack-project-list'] = $p->code;
625      
626     
627         $headers['Subject'] = sprintf("%s#%s %s (%s %s)",
628           $subj, $this->id, $this->summary, $this->status_name, $this->classification_id_name);
629     
630         $owner =  $this->owner_id ? $this->owner_id_name : 'nobody';
631     
632         $body = 
633             sprintf("%s/Ticket/%s\n\n", $ff->MTrackWeb['url'], $this->id) .
634             sprintf("#%s: %s (%s %s)\n", $this->id, $this->summary, $this->status_name, $this->classification_id_name) .
635             sprintf("Responsible: %s (%s / %s)\n", $owner, $this->priority_id_name, $this->severity_id_name) .
636     
637             sprintf("\nDescription:\n%s\n", wordwrap($this->description,80, "\n")) .
638      //sprintf("Milestone: %s\n", join(', ', $T->getMilestones()));
639             //sprintf("Component: %s\n", join(', ', $T->getComponents()));
640             "\n";
641     
642       // Display changed fields grouped by the person that last changed them
643         //$who_changed = array();
644         //foreach ($field_changers as $field => $who) {
645         //  $who_changed[$who][] = $field;
646         //}
647         $body.="\n";
648         foreach ($changes as $change) {
649             
650             $body .= "\n{$change->changedate}:  Change by {$change->person_id_name}\n";
651             $body .= str_repeat('-', 80) . "\n";
652             if (!empty($change->reason)) {
653                 $body.= wordwrap($change->reason, 80, "\n") ."\n\n";
654             }
655             $ar = $change->cachedAudit();
656             
657             foreach($ar as $audit) {
658                 
659                 switch($audit->field()) {
660                     case 'id':
661                         continue 2; //??? ignore?
662                     case '@components':
663                         continue 2;
664                    
665                     case '@milestones':
666                         continue 2;
667                      
668                     case '@keywords':
669                         continue 2;
670                    
671                     //case 'commit?'  
672                       
673                     default:
674                         if ($audit->oldvalue == $audit->value) {
675                             continue 2; // no change?!?!?
676                         }
677                         $oldvalue = $audit->oldvalue($change);
678                         $value = $audit->value($change);
679                         $field = $audit->field();
680                         
681                         $field  = preg_replace('/_id$/', '', $field);
682                         $field  = str_replace('/_/', ' ', $field);
683                         $lb = strpos($oldvalue,"\n") > -1 || strpos($value,"\n") > -1 ? "\n" : '';
684                         
685                         if (!strlen($oldvalue)) {
686                             $body .= "Set {$field} to: {$lb}{$value}\n";
687                             continue 2;
688                         }
689                         
690                         if (!strlen($value)) {
691                             $body .= "Removed {$field} - was: {$lb}{$oldvalue}\n";
692                             continue 2;
693                         }
694                         $body .= "Changed {$field} from : {$lb}{$oldvalue} {$lb}-> {$lb}{$value}\n";
695                         continue 2;  
696                 }
697             }
698         }
699         
700         $body .= sprintf("\n\n%s/Ticket/%s\n\n", $ff->MTrackWeb['url'], $this->id);
701         
702         return array(
703             'body' => $body,
704             'headers' => $headers
705         );
706          
707          
708     }
709     
710     function toEventString()
711     {
712         return '#'.$this->id . ' ' . $this->summary;
713         
714     }
715     
716     var $_mtrack_change = false;
717     
718     function beforeUpdate($old, $req, $roo)
719     {
720         
721          if (!empty($req['reason'])) {
722             $ch = DB_DataObject::factory('mtrack_change');
723             $ch->beginChange($this, $req['reason']);
724             $this->_mtrack_change= $ch;
725         }
726         // used by gitlive close ticket...
727         if (!empty($req['status_name']) && empty($req['status'])) {
728             if ($req['status_name'] == 'resolved') {
729                 $req['status_name']  = 'REVIEW_BY_ADMIN';
730                 // if it's an admin doing it.. then set it to fixed?
731             }
732             $n = DB_DataObject::Factory('core_enum');
733             $n->whereAddIn('name', array( $req['status_name'], strtoupper($req['status_name'])), 'string');
734             $n->whereAddIn('etype', array('ticketstate','resolution'),'string');
735             if (!$n->find(true)) {
736                 $roo->jerr("invalid status name");
737             }
738             $this->status = $n->id;
739             
740         }
741         if ($old->status != $this->status) {
742             // status has been changed. - check to see if it's really a resolution.
743             $n = DB_DataObject::Factory('core_enum');
744             if (!$this->status || !$n->get($this->status)) {
745                 $roo->jerr("status set invalid");
746             }
747             if ($n->etype != 'ticketstate') {
748                 if ($n->etype != 'resolution') {
749                     $roo->jerr("status set invalid");
750                 }
751                 $this->resolution_id = $n->id;
752                 $n = DB_DataObject::Factory('core_enum');
753                 $n->etype = 'ticketstate';
754                 $n->get('name', 'closed'); // no error checking!?!
755                 $this->status = $n->id;
756             }
757             
758         }
759         
760         
761         
762         
763         if (isset($req['_reorder'])) {
764             $old->updateSequence($req['_reorder']);
765             $roo->jok("UPDATED");
766         }
767         
768         if (isset($req['_before']) || isset($req['_after']))  {
769             $req['_after'] = isset($req['_after']) ? $req['_after'] : 0;
770             $req['_before'] = isset($req['_before']) ? $req['_before'] : 0;
771             $old->updateSequence($req['_before'],$req['_after']);
772             
773             
774             if ($req['_before']) { 
775                 $before = DB_DataObject::factory('mtrack_ticket');
776                 $before->get($req['_before']);
777                 $this->seqid = $before->seqid;
778             } else if ($req['_after']) {
779                 $after = DB_DataObject::factory('mtrack_ticket');
780                 $after->get($req['_after']);
781                 $this->seqid =  $after->seqid+1;
782             } else {
783                 
784                 
785                 
786             }
787         }   
788          
789         
790         
791     }
792     
793     function beforeInsert($req, $roo)
794     {
795         
796         if (isset($req['_bulk_update'])) {
797             $this->bulkUpdate($req,$roo);
798             $this->jerr("oops");
799         }
800         
801         if (!$this->status) {
802             $n = DB_DataObject::Factory('core_enum');
803             $n->etype = 'ticketstate';
804             $n->get('name', 'IN_PROGRESS'); 
805             $this->status = $n->id;
806         }
807     }
808     
809     
810     function bulkUpdate($req, $roo)
811     {
812         if (empty($req['_bulk_update'])) {
813             $roo->jerr("invalid tickets");
814         }
815         
816         $t = DB_DAtaObject::Factory('mtrack_ticket');
817         $t->autoJoin();
818         $t->whereAddIn($this->tableName().'.id', explode(',',$req['_bulk_update'] ), 'int');
819         $all = $t->fetchAll();
820         
821         foreach($req as $k=>$v) {
822             if (empty($v)) {
823                 continue;
824             }
825             $cg[$k] = $v;
826         }
827         foreach($all as $t) {
828             $old = clone($t);
829             $t->setFrom($cg);
830             $t->beforeUpdate($old, $cg, $roo);
831             $t->update($old);
832             $t->onUpdate($old,$req, $roo);
833             
834         }
835         $t = DB_DAtaObject::Factory('mtrack_ticket');
836         $t->autoJoin();
837         $t->whereAddIn($this->tableName().'.id', explode(',',$req['_bulk_update'] ), 'int');
838         $roo->jok($t->fetchAll(false,false,'toArray'));
839         
840         
841          
842         
843         
844     }
845     
846     
847     
848     
849     function onUpdate($old,$req, $roo)
850     {
851         //print_r($old); print_r($this);
852         
853          //die("on update");
854         $ch = $this->_mtrack_change; 
855         if (!$ch) {
856             $ch = DB_DataObject::factory('mtrack_change');
857             $ch->beginChange($this, "Changed");
858             $this->_mtrack_change = $ch;
859         }
860         
861         $n = $ch->add($this, $old);
862         if (!$n && !$this->_mtrack_change) {
863             //die("deleting change");
864             $ch->delete(); // do not record the change..
865         } else { 
866         
867             $oo = clone($this);
868             $this->updated = $ch->id;
869             $this->update($oo);
870         }
871         // trigger emails..
872         
873         if (isset($req['uploads'])) {
874             $this->updateUploads(json_decode($req['uploads']),$roo);
875         }
876         
877         
878     }
879     function onInsert($req,$roo, $even)
880     {        
881         $ch = $this->_mtrack_change; 
882         if (!$ch) {
883             $ch = DB_DataObject::factory('mtrack_change');
884             $ch->beginChange($this, "Changed");
885             $this->_mtrack_change = $ch;
886         }
887         $ch->add($this, false);
888         if (isset($req['uploads'])) {
889             $this->updateUploads(json_decode($req['uploads']), $roo);
890         }
891         // in theory this should trigger a timesheet log that we have started working on something.
892         
893         
894         
895     }
896     
897     function updateUploads($uploads, $roo)
898     {
899         foreach($uploads as $up) {
900             
901             if (empty($up->id) || $up->id < 0) {
902                 if ($up->is_deleted) {
903                     continue;
904                 }
905                 
906                 
907                 $i = DB_DAtaObject::factory('Images');
908                 $i->object($this);
909                 $i->setFrom(array(
910                     'mimetype' => $up->mimetype,
911                     'title' => $up->title,
912                     'filename' => $up->title,
913                     'created' => date('Y-m-d H:i:s')
914                 ));
915                 $i->createFromData($up->srcdata);
916                 continue;
917             }
918             // edit..
919             $i = DB_DAtaObject::factory('Images');
920             $i->object($this);
921             if (!$i->get($up->id)) {
922                 $roo->jerr("invalid update id for image {$up->id}");
923             }
924             if ($up->is_deleted) {
925             
926                 $i->beforeDelete(array(), $roo);
927                 $i->delete();
928             }
929                
930             // update???
931             
932         }
933     }
934     
935     
936     function updated()
937     {
938         $ch = DB_DataObject::factory('mtrack_change');
939         $ch->get($this->updated ? $this->updated : $this->created);
940         return $ch;
941         
942     }
943     
944     
945     function toRooSingleArray($au, $req)
946     {
947         if ($this->id) {
948             $ret = $this->toArray();;
949             $ret['images'] = array();
950             foreach($this->images() as $i) {
951                 $ret['images'][] = $i->toRooArray(array());
952             }
953             
954             return $ret;
955         }
956         // empty!
957         $p = $this->factory('core_enum');
958         $p->etype='priority';
959         $p->get('name', 'normal');
960         
961         $s = $this->factory('core_enum');
962         $s->etype='severity';
963         $s->get('name', 'normal');
964         
965         $c = $this->factory('core_enum');
966         $c->etype='classification';
967         $c->get('name', 'enhancement');
968         
969         $this->severity_id = $s->id;
970         $this->classification_id = $c->id;
971         $this->priority_id = $p->id;
972         $ret = $this->toArray();
973         $ret = array_merge($ret, $p->toArray('priority_id_%s'));
974         $ret = array_merge($ret, $c->toArray('classification_id_%s'));
975         $ret = array_merge($ret, $s->toArray('severity_id_%s'));
976         
977        
978         return $ret;
979         
980     }
981     
982     //  http://roojs.com/admin.php/Roo/Mtrack_ticket?_post=1&id=733&_reorder=0
983     function updateSequence($tbefore, $tafter = 0)
984     {
985         // fix empty sequences?
986         
987         $tn = $this->tableName();
988         $t = DB_DataObject::factory('mtrack_ticket');
989         $tt = clone($t);
990         $t->seqid = 0;
991         //$t->project_id = $this->project_id;
992         $t->autoJoin();
993         $t->selectAdd();
994         // we still apply to closed tickets????
995          $t->whereAddIn('join_status_id.name', array(  'CLOSED'), 'string');
996         $t->applySort(false,  'old_style', false);
997         
998         $old_seqid = $this->seqid;
999         
1000         if ($t->count()) {
1001             // add a seq id to everything.
1002             $t->selectAdd("{$tn}.id as id");
1003             $ids = $t->fetchAll('id');
1004             
1005             $t_all = DB_DataObject::factory('mtrack_ticket');
1006             $t_all->selectAdd('max(seqid) as seqid');
1007             $t_all->find(true);
1008             
1009             $start = empty($t_all->seqid) ? 1 : $t_all->seqid;
1010             $start++;
1011             foreach($ids as $i => $id) {
1012                 $tt->query("UPDATE {$tn} SET seqid = " . ($start + $i) ." WHERE id = {$id}");
1013             }
1014             $t = DB_DataObject::Factory('mtrack_ticket');
1015             $t->get($this->id);
1016             $old_seqid = $t->seqid;
1017             
1018         }
1019         
1020         
1021         
1022         
1023         $before_seq_id = 0;
1024         
1025         if ($tbefore) { 
1026             $before = DB_DataObject::factory('mtrack_ticket');
1027             $before->get($tbefore);
1028             $before_seq_id = $before->seqid;
1029             
1030         }
1031         if ($tafter) { 
1032             $after = DB_DataObject::factory('mtrack_ticket');
1033             $after->get($tafter);
1034             $before_seq_id = $after->seqid+1;
1035         }
1036          
1037         $t = DB_DataObject::factory('mtrack_ticket');
1038         
1039         
1040         /// move everything down.
1041         $t->query("UPDATE {$tn} SET
1042                         seqid = seqid + 1
1043                     WHERE
1044                        seqid > {$before_seq_id}
1045                 ");
1046         
1047         // ...1234 GAP 6789
1048         
1049         // puts this one in the gap..
1050         $t->query("UPDATE {$tn} SET seqid = ". ( $before_seq_id  + 1) . " WHERE id = {$this->id}");
1051         
1052         // now there is a gap where this used to be..
1053         
1054         
1055          $t->query("UPDATE {$tn} SET
1056                         seqid = seqid - 1
1057                    WHERE 
1058                         seqid > {$old_seqid}
1059         ");             
1060          
1061         
1062     }
1063     function toRooArray($q) {
1064         if (empty($q['_future_schedule'])) {
1065             return $this->toArray();
1066         }
1067         if (!$this->developer_id) {
1068             return false;
1069         }
1070         $add = $this->toCalEvent();
1071         if (isset($q['filter'])) {
1072             foreach($q['filter'] as $k=>$v) {
1073                 if (empty($v)) {
1074                     continue;
1075                 }
1076                 if (!isset($add[$k])) {
1077                     $roo->jerr('invalid filter ' .$k);
1078                 }
1079                 if ($v != $add[$k]) {
1080                     $add = false;
1081                 }
1082             }
1083         }
1084         return $add;
1085      
1086     }
1087      
1088     
1089     
1090     // only run this once..
1091     function toCalEvent()
1092     {
1093         //secho '<PRE>';print_r($this);exit;
1094         static $cdata = array();
1095         
1096         
1097         if (empty($cdata[$this->developer_id])) {
1098             $start = $this->availStart(date('Y-m-d H:i:s'));
1099         } else {
1100             $start = $this->availStart($cdata[$this->developer_id]);
1101         }
1102         
1103         $end = $this->availEnd($start, $this->estimated ? $this->estimated : 1);
1104         $cdata[$this->developer_id]= $end;
1105         
1106         $ret = array_merge($this->toArray(), array(
1107             'start_dt' => $start,
1108             'end_dt' =>  $end,
1109             //'start_time' => 
1110             'title' => '#' . $this->id . '  ' .$this->developer_id_name . ' : ' . $this->project_id_name,
1111             'description' => $this->summary,
1112           
1113         ));
1114         return $ret;
1115         
1116         
1117         
1118     }
1119     function availStart($dt)
1120     {
1121         //give a date time.. see if we should schedule it today?
1122         $st = strtotime($dt);
1123         
1124         if (date('H', $st) < 11) {
1125             $st = strtotime(date('Y-m-d 10:00:00',$st));
1126         }
1127         if (date('H', $st) > 17) {
1128             if(date('N',$st) > 4) {
1129                 // it's friday... (5) +3 , sat (6) +2 sun(7) +1
1130                 $days = 8 - date('N',$st);
1131                 $st = strtotime(date('Y-m-d',$st).' + ' . $days  . ' DAY');
1132                 
1133             } else {
1134                 $st = strtotime(date('Y-m-d',$st).' + 1 DAY');
1135             }
1136             $st = strtotime(date('Y-m-d 10:00:00',$st));
1137             return date('Y-m-d H:i:s', $st);
1138         }
1139         // otherwise this time is OK...
1140         return $dt;
1141     }
1142     function availEnd($start, $hours) {
1143         
1144         $et = $start;
1145         for ($i = 0; $i < ceil($hours ); $i++) {
1146             $et = $this->availStart(date('Y-m-d H:i:s', strtotime($et . ' + 1 HOUR')));
1147         }
1148         return $et;
1149          
1150     }
1151     
1152     function optionsForProject($roo,$pid)
1153     {
1154         $ret = array();
1155         $m = DB_DataObject::factory('mtrack_milestone');
1156         $m->project_id = $pid;
1157         $m->selectAdd();
1158         $m->selectAdd('id,name as name, name as display_name');
1159         $m->orderBy('startdate ASC');
1160         $ret['milestone'] =  $m->fetchAll(false,false,'toArray');
1161         
1162         $mm = DB_DataObject::factory('core_enum');
1163         $mm->orderBy('seqid ASC');
1164         $mm->active = 1;
1165         
1166         $m = clone($mm);
1167         $m->etype = 'priority';
1168         $ret['priority'] =  $m->fetchAll(false,false,'toArray');
1169         
1170         $m = clone($mm);
1171         $m->etype = 'severity';
1172         $ret['severity'] =  $m->fetchAll(false,false,'toArray');
1173         
1174         $m = clone($mm);
1175         $m->etype = 'classification';
1176         $ret['classification'] =  $m->fetchAll(false,false,'toArray');
1177
1178         
1179         
1180         $m = DB_DataObject::factory('core_person');
1181         $m->company_id = $roo->company->id;
1182         $m->selectAdd();
1183         $m->selectAdd("id,name as name, concat(name ,' <', email , '>') as display_name");
1184         $m->active = 1;
1185         $m->applyFilters(array(
1186             'query' => array(
1187                 'project_id' => $pid,
1188                 'role' => 'DEVELOPER'
1189             )
1190         ),$roo->authUser, $roo);
1191                                                 
1192         $m->orderBy('name ASC');
1193         $ret['developer'] =  $m->fetchAll(false,false,'toArray');
1194         $ret['authuser_id'] =  "".$roo->authUser->id; //string...
1195         
1196         $roo->jok($ret);
1197     }
1198  
1199 }